forked from cybrespace/pinafore
		
	fix statuses being deleted from threads
This commit is contained in:
		
							parent
							
								
									23a247a8c2
								
							
						
					
					
						commit
						da2daa955d
					
				
					 11 changed files with 256 additions and 129 deletions
				
			
		| 
						 | 
					@ -35,9 +35,7 @@ async function doDeleteStatus (instanceName, statusId) {
 | 
				
			||||||
  let rebloggedIds = await getIdsThatRebloggedThisStatus(instanceName, statusId)
 | 
					  let rebloggedIds = await getIdsThatRebloggedThisStatus(instanceName, statusId)
 | 
				
			||||||
  let statusIdsToDelete = Array.from(new Set([statusId].concat(rebloggedIds).filter(Boolean)))
 | 
					  let statusIdsToDelete = Array.from(new Set([statusId].concat(rebloggedIds).filter(Boolean)))
 | 
				
			||||||
  let notificationIdsToDelete = new Set(await getNotificationIdsForStatuses(instanceName, statusIdsToDelete))
 | 
					  let notificationIdsToDelete = new Set(await getNotificationIdsForStatuses(instanceName, statusIdsToDelete))
 | 
				
			||||||
  await Promise.all([
 | 
					  await deleteStatusesAndNotifications(instanceName, statusIdsToDelete, notificationIdsToDelete)
 | 
				
			||||||
    deleteStatusesAndNotifications(instanceName, statusIdsToDelete, notificationIdsToDelete)
 | 
					 | 
				
			||||||
  ])
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function deleteStatus (instanceName, statusId) {
 | 
					export function deleteStatus (instanceName, statusId) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,8 @@ import {
 | 
				
			||||||
import debounce from 'lodash/debounce'
 | 
					import debounce from 'lodash/debounce'
 | 
				
			||||||
import { store } from '../_store/store'
 | 
					import { store } from '../_store/store'
 | 
				
			||||||
import { mark, stop } from '../_utils/marks'
 | 
					import { mark, stop } from '../_utils/marks'
 | 
				
			||||||
 | 
					import { deleteAll } from './utils'
 | 
				
			||||||
 | 
					import { createPinnedStatusKeyRange, createThreadKeyRange } from './keys'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const BATCH_SIZE = 20
 | 
					const BATCH_SIZE = 20
 | 
				
			||||||
const TIME_AGO = 14 * 24 * 60 * 60 * 1000 // two weeks ago
 | 
					const TIME_AGO = 14 * 24 * 60 * 60 * 1000 // two weeks ago
 | 
				
			||||||
| 
						 | 
					@ -34,18 +36,20 @@ function batchedGetAll (callGetAll, callback) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function cleanupStatuses (statusesStore, statusTimelinesStore, threadsStore, cutoff) {
 | 
					function cleanupStatuses (statusesStore, statusTimelinesStore, threadsStore, cutoff) {
 | 
				
			||||||
  batchedGetAll(
 | 
					  batchedGetAll(
 | 
				
			||||||
    () => statusesStore.index(TIMESTAMP).getAll(IDBKeyRange.upperBound(cutoff), BATCH_SIZE),
 | 
					    () => statusesStore.index(TIMESTAMP).getAllKeys(IDBKeyRange.upperBound(cutoff), BATCH_SIZE),
 | 
				
			||||||
    results => {
 | 
					    results => {
 | 
				
			||||||
      results.forEach(result => {
 | 
					      results.forEach(statusId => {
 | 
				
			||||||
        statusesStore.delete(result.id)
 | 
					        statusesStore.delete(statusId)
 | 
				
			||||||
        threadsStore.delete(result.id)
 | 
					        deleteAll(
 | 
				
			||||||
        let req = statusTimelinesStore.index('statusId').getAllKeys(IDBKeyRange.only(result.id))
 | 
					          statusTimelinesStore,
 | 
				
			||||||
        req.onsuccess = e => {
 | 
					          statusTimelinesStore.index('statusId'),
 | 
				
			||||||
          let keys = e.target.result
 | 
					          IDBKeyRange.only(statusId)
 | 
				
			||||||
          keys.forEach(key => {
 | 
					        )
 | 
				
			||||||
            statusTimelinesStore.delete(key)
 | 
					        deleteAll(
 | 
				
			||||||
          })
 | 
					          threadsStore,
 | 
				
			||||||
        }
 | 
					          threadsStore,
 | 
				
			||||||
 | 
					          createThreadKeyRange(statusId)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
| 
						 | 
					@ -53,17 +57,15 @@ function cleanupStatuses (statusesStore, statusTimelinesStore, threadsStore, cut
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function cleanupNotifications (notificationsStore, notificationTimelinesStore, cutoff) {
 | 
					function cleanupNotifications (notificationsStore, notificationTimelinesStore, cutoff) {
 | 
				
			||||||
  batchedGetAll(
 | 
					  batchedGetAll(
 | 
				
			||||||
    () => notificationsStore.index(TIMESTAMP).getAll(IDBKeyRange.upperBound(cutoff), BATCH_SIZE),
 | 
					    () => notificationsStore.index(TIMESTAMP).getAllKeys(IDBKeyRange.upperBound(cutoff), BATCH_SIZE),
 | 
				
			||||||
    results => {
 | 
					    results => {
 | 
				
			||||||
      results.forEach(result => {
 | 
					      results.forEach(notificationId => {
 | 
				
			||||||
        notificationsStore.delete(result.id)
 | 
					        notificationsStore.delete(notificationId)
 | 
				
			||||||
        let req = notificationTimelinesStore.index('notificationId').getAllKeys(IDBKeyRange.only(result.id))
 | 
					        deleteAll(
 | 
				
			||||||
        req.onsuccess = e => {
 | 
					          notificationTimelinesStore,
 | 
				
			||||||
          let keys = e.target.result
 | 
					          notificationTimelinesStore.index('notificationId'),
 | 
				
			||||||
          keys.forEach(key => {
 | 
					          IDBKeyRange.only(notificationId)
 | 
				
			||||||
            notificationTimelinesStore.delete(key)
 | 
					        )
 | 
				
			||||||
          })
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
| 
						 | 
					@ -71,18 +73,15 @@ function cleanupNotifications (notificationsStore, notificationTimelinesStore, c
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function cleanupAccounts (accountsStore, pinnedStatusesStore, cutoff) {
 | 
					function cleanupAccounts (accountsStore, pinnedStatusesStore, cutoff) {
 | 
				
			||||||
  batchedGetAll(
 | 
					  batchedGetAll(
 | 
				
			||||||
    () => accountsStore.index(TIMESTAMP).getAll(IDBKeyRange.upperBound(cutoff), BATCH_SIZE),
 | 
					    () => accountsStore.index(TIMESTAMP).getAllKeys(IDBKeyRange.upperBound(cutoff), BATCH_SIZE),
 | 
				
			||||||
    (results) => {
 | 
					    results => {
 | 
				
			||||||
      results.forEach(result => {
 | 
					      results.forEach(accountId => {
 | 
				
			||||||
        accountsStore.delete(result.id)
 | 
					        accountsStore.delete(accountId)
 | 
				
			||||||
        let keyRange = IDBKeyRange.bound(result.id + '\u0000', result.id + '\u0000\uffff')
 | 
					        deleteAll(
 | 
				
			||||||
        let req = pinnedStatusesStore.getAllKeys(keyRange)
 | 
					          pinnedStatusesStore,
 | 
				
			||||||
        req.onsuccess = e => {
 | 
					          pinnedStatusesStore,
 | 
				
			||||||
          let keys = e.target.result
 | 
					          createPinnedStatusKeyRange(accountId)
 | 
				
			||||||
          keys.forEach(key => {
 | 
					        )
 | 
				
			||||||
            pinnedStatusesStore.delete(key)
 | 
					 | 
				
			||||||
          })
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
| 
						 | 
					@ -90,10 +89,10 @@ function cleanupAccounts (accountsStore, pinnedStatusesStore, cutoff) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function cleanupRelationships (relationshipsStore, cutoff) {
 | 
					function cleanupRelationships (relationshipsStore, cutoff) {
 | 
				
			||||||
  batchedGetAll(
 | 
					  batchedGetAll(
 | 
				
			||||||
    () => relationshipsStore.index(TIMESTAMP).getAll(IDBKeyRange.upperBound(cutoff), BATCH_SIZE),
 | 
					    () => relationshipsStore.index(TIMESTAMP).getAllKeys(IDBKeyRange.upperBound(cutoff), BATCH_SIZE),
 | 
				
			||||||
    (results) => {
 | 
					    results => {
 | 
				
			||||||
      results.forEach(result => {
 | 
					      results.forEach(relationshipId => {
 | 
				
			||||||
        relationshipsStore.delete(result.id)
 | 
					        relationshipsStore.delete(relationshipId)
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,12 @@
 | 
				
			||||||
export const STATUSES_STORE = 'statuses-v1'
 | 
					export const STATUSES_STORE = 'statuses-v2'
 | 
				
			||||||
export const STATUS_TIMELINES_STORE = 'status_timelines-v1'
 | 
					export const STATUS_TIMELINES_STORE = 'status_timelines-v2'
 | 
				
			||||||
export const META_STORE = 'meta-v1'
 | 
					export const META_STORE = 'meta-v2'
 | 
				
			||||||
export const ACCOUNTS_STORE = 'accounts-v1'
 | 
					export const ACCOUNTS_STORE = 'accounts-v2'
 | 
				
			||||||
export const RELATIONSHIPS_STORE = 'relationships-v1'
 | 
					export const RELATIONSHIPS_STORE = 'relationships-v2'
 | 
				
			||||||
export const NOTIFICATIONS_STORE = 'notifications-v1'
 | 
					export const NOTIFICATIONS_STORE = 'notifications-v2'
 | 
				
			||||||
export const NOTIFICATION_TIMELINES_STORE = 'notification_timelines-v1'
 | 
					export const NOTIFICATION_TIMELINES_STORE = 'notification_timelines-v2'
 | 
				
			||||||
export const PINNED_STATUSES_STORE = 'pinned_statuses-v1'
 | 
					export const PINNED_STATUSES_STORE = 'pinned_statuses-v2'
 | 
				
			||||||
export const THREADS_STORE = 'threads-v1'
 | 
					export const THREADS_STORE = 'threads-v2'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const TIMESTAMP = '__pinafore_ts'
 | 
					export const TIMESTAMP = '__pinafore_ts'
 | 
				
			||||||
export const ACCOUNT_ID = '__pinafore_acct_id'
 | 
					export const ACCOUNT_ID = '__pinafore_acct_id'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,10 +13,12 @@ import {
 | 
				
			||||||
  STATUS_ID
 | 
					  STATUS_ID
 | 
				
			||||||
} from './constants'
 | 
					} from './constants'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import forEach from 'lodash/forEach'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const openReqs = {}
 | 
					const openReqs = {}
 | 
				
			||||||
const databaseCache = {}
 | 
					const databaseCache = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const DB_VERSION = 6
 | 
					const DB_VERSION = 7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getDatabase (instanceName) {
 | 
					export function getDatabase (instanceName) {
 | 
				
			||||||
  if (!instanceName) {
 | 
					  if (!instanceName) {
 | 
				
			||||||
| 
						 | 
					@ -35,28 +37,46 @@ export function getDatabase (instanceName) {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    req.onupgradeneeded = (e) => {
 | 
					    req.onupgradeneeded = (e) => {
 | 
				
			||||||
      let db = req.result
 | 
					      let db = req.result
 | 
				
			||||||
      let tx = e.currentTarget.transaction
 | 
					
 | 
				
			||||||
      if (e.oldVersion < 5) {
 | 
					      function createObjectStore (name, init, indexes) {
 | 
				
			||||||
        db.createObjectStore(STATUSES_STORE, {keyPath: 'id'})
 | 
					        let store = init
 | 
				
			||||||
          .createIndex(TIMESTAMP, TIMESTAMP)
 | 
					          ? db.createObjectStore(name, init)
 | 
				
			||||||
        db.createObjectStore(STATUS_TIMELINES_STORE)
 | 
					          : db.createObjectStore(name)
 | 
				
			||||||
          .createIndex('statusId', '')
 | 
					        if (indexes) {
 | 
				
			||||||
        db.createObjectStore(NOTIFICATIONS_STORE, {keyPath: 'id'})
 | 
					          forEach(indexes, (indexValue, indexKey) => {
 | 
				
			||||||
          .createIndex(TIMESTAMP, TIMESTAMP)
 | 
					            store.createIndex(indexKey, indexValue)
 | 
				
			||||||
        db.createObjectStore(NOTIFICATION_TIMELINES_STORE)
 | 
					          })
 | 
				
			||||||
          .createIndex('notificationId', '')
 | 
					 | 
				
			||||||
        db.createObjectStore(ACCOUNTS_STORE, {keyPath: 'id'})
 | 
					 | 
				
			||||||
          .createIndex(TIMESTAMP, TIMESTAMP)
 | 
					 | 
				
			||||||
        db.createObjectStore(RELATIONSHIPS_STORE, {keyPath: 'id'})
 | 
					 | 
				
			||||||
          .createIndex(TIMESTAMP, TIMESTAMP)
 | 
					 | 
				
			||||||
        db.createObjectStore(META_STORE)
 | 
					 | 
				
			||||||
        db.createObjectStore(PINNED_STATUSES_STORE)
 | 
					 | 
				
			||||||
        tx.objectStore(STATUSES_STORE).createIndex(REBLOG_ID, REBLOG_ID)
 | 
					 | 
				
			||||||
        tx.objectStore(NOTIFICATIONS_STORE).createIndex('statusId', 'statusId')
 | 
					 | 
				
			||||||
        db.createObjectStore(THREADS_STORE)
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      if (e.oldVersion < 6) {
 | 
					      }
 | 
				
			||||||
        tx.objectStore(NOTIFICATIONS_STORE).createIndex(STATUS_ID, STATUS_ID)
 | 
					
 | 
				
			||||||
 | 
					      if (e.oldVersion < 7) {
 | 
				
			||||||
 | 
					        createObjectStore(STATUSES_STORE, {keyPath: 'id'}, {
 | 
				
			||||||
 | 
					          [TIMESTAMP]: TIMESTAMP,
 | 
				
			||||||
 | 
					          [REBLOG_ID]: REBLOG_ID
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        createObjectStore(STATUS_TIMELINES_STORE, null, {
 | 
				
			||||||
 | 
					          'statusId': ''
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        createObjectStore(NOTIFICATIONS_STORE, {keyPath: 'id'}, {
 | 
				
			||||||
 | 
					          [TIMESTAMP]: TIMESTAMP,
 | 
				
			||||||
 | 
					          [STATUS_ID]: STATUS_ID
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        createObjectStore(NOTIFICATION_TIMELINES_STORE, null, {
 | 
				
			||||||
 | 
					          'notificationId': ''
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        createObjectStore(ACCOUNTS_STORE, {keyPath: 'id'}, {
 | 
				
			||||||
 | 
					          [TIMESTAMP]: TIMESTAMP
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        createObjectStore(RELATIONSHIPS_STORE, {keyPath: 'id'}, {
 | 
				
			||||||
 | 
					          [TIMESTAMP]: TIMESTAMP
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        createObjectStore(THREADS_STORE, null, {
 | 
				
			||||||
 | 
					          'statusId': ''
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        createObjectStore(PINNED_STATUSES_STORE, null, {
 | 
				
			||||||
 | 
					          'statusId': ''
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        createObjectStore(META_STORE)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    req.onsuccess = () => resolve(req.result)
 | 
					    req.onsuccess = () => resolve(req.result)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										47
									
								
								routes/_database/keys.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								routes/_database/keys.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,47 @@
 | 
				
			||||||
 | 
					import { toReversePaddedBigInt, zeroPad } from '../_utils/sorting'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// timelines
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createTimelineId (timeline, id) {
 | 
				
			||||||
 | 
					  // reverse chronological order, prefixed by timeline
 | 
				
			||||||
 | 
					  return timeline + '\u0000' + toReversePaddedBigInt(id)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createTimelineKeyRange (timeline, maxId) {
 | 
				
			||||||
 | 
					  let negBigInt = maxId && toReversePaddedBigInt(maxId)
 | 
				
			||||||
 | 
					  let start = negBigInt ? (timeline + '\u0000' + negBigInt) : (timeline + '\u0000')
 | 
				
			||||||
 | 
					  let end = timeline + '\u0000\uffff'
 | 
				
			||||||
 | 
					  return IDBKeyRange.bound(start, end, true, true)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// threads
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createThreadId (statusId, i) {
 | 
				
			||||||
 | 
					  return statusId + '\u0000' + zeroPad(i, 5)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createThreadKeyRange (statusId) {
 | 
				
			||||||
 | 
					  return IDBKeyRange.bound(
 | 
				
			||||||
 | 
					    statusId + '\u0000',
 | 
				
			||||||
 | 
					    statusId + '\u0000\uffff'
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// pinned statues
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createPinnedStatusId (accountId, i) {
 | 
				
			||||||
 | 
					  return accountId + '\u0000' + zeroPad(i, 3)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createPinnedStatusKeyRange (accountId) {
 | 
				
			||||||
 | 
					  return IDBKeyRange.bound(
 | 
				
			||||||
 | 
					    accountId + '\u0000',
 | 
				
			||||||
 | 
					    accountId + '\u0000\uffff'
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import { toPaddedBigInt, toReversePaddedBigInt } from '../_utils/sorting'
 | 
					import difference from 'lodash/difference'
 | 
				
			||||||
 | 
					import times from 'lodash/times'
 | 
				
			||||||
import { cloneForStorage } from './helpers'
 | 
					import { cloneForStorage } from './helpers'
 | 
				
			||||||
import { dbPromise, getDatabase } from './databaseLifecycle'
 | 
					import { dbPromise, getDatabase } from './databaseLifecycle'
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					@ -16,13 +17,15 @@ import {
 | 
				
			||||||
  REBLOG_ID,
 | 
					  REBLOG_ID,
 | 
				
			||||||
  STATUS_ID, THREADS_STORE
 | 
					  STATUS_ID, THREADS_STORE
 | 
				
			||||||
} from './constants'
 | 
					} from './constants'
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
function createTimelineKeyRange (timeline, maxId) {
 | 
					  createThreadKeyRange,
 | 
				
			||||||
  let negBigInt = maxId && toReversePaddedBigInt(maxId)
 | 
					  createTimelineKeyRange,
 | 
				
			||||||
  let start = negBigInt ? (timeline + '\u0000' + negBigInt) : (timeline + '\u0000')
 | 
					  createTimelineId,
 | 
				
			||||||
  let end = timeline + '\u0000\uffff'
 | 
					  createThreadId,
 | 
				
			||||||
  return IDBKeyRange.bound(start, end, true, true)
 | 
					  createPinnedStatusKeyRange,
 | 
				
			||||||
}
 | 
					  createPinnedStatusId
 | 
				
			||||||
 | 
					} from './keys'
 | 
				
			||||||
 | 
					import { deleteAll } from './utils'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function cacheStatus (status, instanceName) {
 | 
					function cacheStatus (status, instanceName) {
 | 
				
			||||||
  setInCache(statusesCache, instanceName, status.id, status)
 | 
					  setInCache(statusesCache, instanceName, status.id, status)
 | 
				
			||||||
| 
						 | 
					@ -80,7 +83,8 @@ async function getStatusThread (instanceName, statusId) {
 | 
				
			||||||
  const db = await getDatabase(instanceName)
 | 
					  const db = await getDatabase(instanceName)
 | 
				
			||||||
  return dbPromise(db, storeNames, 'readonly', (stores, callback) => {
 | 
					  return dbPromise(db, storeNames, 'readonly', (stores, callback) => {
 | 
				
			||||||
    let [ threadsStore, statusesStore, accountsStore ] = stores
 | 
					    let [ threadsStore, statusesStore, accountsStore ] = stores
 | 
				
			||||||
    threadsStore.get(statusId).onsuccess = e => {
 | 
					    let keyRange = createThreadKeyRange(statusId)
 | 
				
			||||||
 | 
					    threadsStore.getAll(keyRange).onsuccess = e => {
 | 
				
			||||||
      let thread = e.target.result
 | 
					      let thread = e.target.result
 | 
				
			||||||
      let res = new Array(thread.length)
 | 
					      let res = new Array(thread.length)
 | 
				
			||||||
      thread.forEach((otherStatusId, i) => {
 | 
					      thread.forEach((otherStatusId, i) => {
 | 
				
			||||||
| 
						 | 
					@ -177,11 +181,6 @@ function fetchNotification (notificationsStore, statusesStore, accountsStore, id
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function createTimelineId (timeline, id) {
 | 
					 | 
				
			||||||
  // reverse chronological order, prefixed by timeline
 | 
					 | 
				
			||||||
  return timeline + '\u0000' + toReversePaddedBigInt(id)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function insertTimelineNotifications (instanceName, timeline, notifications) {
 | 
					async function insertTimelineNotifications (instanceName, timeline, notifications) {
 | 
				
			||||||
  for (let notification of notifications) {
 | 
					  for (let notification of notifications) {
 | 
				
			||||||
    setInCache(notificationsCache, instanceName, notification.id, notification)
 | 
					    setInCache(notificationsCache, instanceName, notification.id, notification)
 | 
				
			||||||
| 
						 | 
					@ -224,10 +223,18 @@ async function insertStatusThread (instanceName, statusId, statuses) {
 | 
				
			||||||
  let storeNames = [THREADS_STORE, STATUSES_STORE, ACCOUNTS_STORE]
 | 
					  let storeNames = [THREADS_STORE, STATUSES_STORE, ACCOUNTS_STORE]
 | 
				
			||||||
  await dbPromise(db, storeNames, 'readwrite', (stores) => {
 | 
					  await dbPromise(db, storeNames, 'readwrite', (stores) => {
 | 
				
			||||||
    let [ threadsStore, statusesStore, accountsStore ] = stores
 | 
					    let [ threadsStore, statusesStore, accountsStore ] = stores
 | 
				
			||||||
    threadsStore.put(statuses.map(_ => _.id), statusId)
 | 
					    threadsStore.getAllKeys(createThreadKeyRange(statusId)).onsuccess = e => {
 | 
				
			||||||
    for (let status of statuses) {
 | 
					      let existingKeys = e.target.result
 | 
				
			||||||
      storeStatus(statusesStore, accountsStore, status)
 | 
					      let newKeys = times(statuses.length, i => createThreadId(statusId, i))
 | 
				
			||||||
 | 
					      let keysToDelete = difference(existingKeys, newKeys)
 | 
				
			||||||
 | 
					      for (let key of keysToDelete) {
 | 
				
			||||||
 | 
					        threadsStore.delete(key)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    statuses.forEach((otherStatus, i) => {
 | 
				
			||||||
 | 
					      storeStatus(statusesStore, accountsStore, otherStatus)
 | 
				
			||||||
 | 
					      threadsStore.put(otherStatus.id, createThreadId(statusId, i))
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -323,7 +330,8 @@ export async function deleteStatusesAndNotifications (instanceName, statusIds, n
 | 
				
			||||||
    STATUS_TIMELINES_STORE,
 | 
					    STATUS_TIMELINES_STORE,
 | 
				
			||||||
    NOTIFICATIONS_STORE,
 | 
					    NOTIFICATIONS_STORE,
 | 
				
			||||||
    NOTIFICATION_TIMELINES_STORE,
 | 
					    NOTIFICATION_TIMELINES_STORE,
 | 
				
			||||||
    PINNED_STATUSES_STORE
 | 
					    PINNED_STATUSES_STORE,
 | 
				
			||||||
 | 
					    THREADS_STORE
 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
  await dbPromise(db, storeNames, 'readwrite', (stores) => {
 | 
					  await dbPromise(db, storeNames, 'readwrite', (stores) => {
 | 
				
			||||||
    let [
 | 
					    let [
 | 
				
			||||||
| 
						 | 
					@ -331,33 +339,41 @@ export async function deleteStatusesAndNotifications (instanceName, statusIds, n
 | 
				
			||||||
      statusTimelinesStore,
 | 
					      statusTimelinesStore,
 | 
				
			||||||
      notificationsStore,
 | 
					      notificationsStore,
 | 
				
			||||||
      notificationTimelinesStore,
 | 
					      notificationTimelinesStore,
 | 
				
			||||||
      pinnedStatusesStore
 | 
					      pinnedStatusesStore,
 | 
				
			||||||
 | 
					      threadsStore
 | 
				
			||||||
    ] = stores
 | 
					    ] = stores
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function deleteStatus (statusId) {
 | 
					    function deleteStatus (statusId) {
 | 
				
			||||||
      pinnedStatusesStore.delete(statusId).onerror = e => {
 | 
					 | 
				
			||||||
        e.preventDefault()
 | 
					 | 
				
			||||||
        e.stopPropagation()
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      statusesStore.delete(statusId)
 | 
					      statusesStore.delete(statusId)
 | 
				
			||||||
      let getAllReq = statusTimelinesStore.index('statusId')
 | 
					      deleteAll(
 | 
				
			||||||
        .getAllKeys(IDBKeyRange.only(statusId))
 | 
					        pinnedStatusesStore,
 | 
				
			||||||
      getAllReq.onsuccess = e => {
 | 
					        pinnedStatusesStore.index('statusId'),
 | 
				
			||||||
        for (let result of e.target.result) {
 | 
					        IDBKeyRange.only(statusId)
 | 
				
			||||||
          statusTimelinesStore.delete(result)
 | 
					      )
 | 
				
			||||||
        }
 | 
					      deleteAll(
 | 
				
			||||||
      }
 | 
					        statusTimelinesStore,
 | 
				
			||||||
 | 
					        statusTimelinesStore.index('statusId'),
 | 
				
			||||||
 | 
					        IDBKeyRange.only(statusId)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      deleteAll(
 | 
				
			||||||
 | 
					        threadsStore,
 | 
				
			||||||
 | 
					        threadsStore.index('statusId'),
 | 
				
			||||||
 | 
					        IDBKeyRange.only(statusId)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      deleteAll(
 | 
				
			||||||
 | 
					        threadsStore,
 | 
				
			||||||
 | 
					        threadsStore,
 | 
				
			||||||
 | 
					        createThreadKeyRange(statusId)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function deleteNotification (notificationId) {
 | 
					    function deleteNotification (notificationId) {
 | 
				
			||||||
      notificationsStore.delete(notificationId)
 | 
					      notificationsStore.delete(notificationId)
 | 
				
			||||||
      let getAllReq = notificationTimelinesStore.index('statusId')
 | 
					      deleteAll(
 | 
				
			||||||
        .getAllKeys(IDBKeyRange.only(notificationId))
 | 
					        notificationTimelinesStore,
 | 
				
			||||||
      getAllReq.onsuccess = e => {
 | 
					        notificationTimelinesStore.index('statusId'),
 | 
				
			||||||
        for (let result of e.target.result) {
 | 
					        IDBKeyRange.only(notificationId)
 | 
				
			||||||
          notificationTimelinesStore.delete(result)
 | 
					      )
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (let statusId of statusIds) {
 | 
					    for (let statusId of statusIds) {
 | 
				
			||||||
| 
						 | 
					@ -383,7 +399,7 @@ export async function insertPinnedStatuses (instanceName, accountId, statuses) {
 | 
				
			||||||
    let [ pinnedStatusesStore, statusesStore, accountsStore ] = stores
 | 
					    let [ pinnedStatusesStore, statusesStore, accountsStore ] = stores
 | 
				
			||||||
    statuses.forEach((status, i) => {
 | 
					    statuses.forEach((status, i) => {
 | 
				
			||||||
      storeStatus(statusesStore, accountsStore, status)
 | 
					      storeStatus(statusesStore, accountsStore, status)
 | 
				
			||||||
      pinnedStatusesStore.put(status.id, accountId + '\u0000' + toPaddedBigInt(i))
 | 
					      pinnedStatusesStore.put(status.id, createPinnedStatusId(accountId, i))
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -393,10 +409,7 @@ export async function getPinnedStatuses (instanceName, accountId) {
 | 
				
			||||||
  const db = await getDatabase(instanceName)
 | 
					  const db = await getDatabase(instanceName)
 | 
				
			||||||
  return dbPromise(db, storeNames, 'readonly', (stores, callback) => {
 | 
					  return dbPromise(db, storeNames, 'readonly', (stores, callback) => {
 | 
				
			||||||
    let [ pinnedStatusesStore, statusesStore, accountsStore ] = stores
 | 
					    let [ pinnedStatusesStore, statusesStore, accountsStore ] = stores
 | 
				
			||||||
    let keyRange = IDBKeyRange.bound(
 | 
					    let keyRange = createPinnedStatusKeyRange(accountId)
 | 
				
			||||||
      accountId + '\u0000',
 | 
					 | 
				
			||||||
      accountId + '\u0000\uffff'
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    pinnedStatusesStore.getAll(keyRange).onsuccess = e => {
 | 
					    pinnedStatusesStore.getAll(keyRange).onsuccess = e => {
 | 
				
			||||||
      let pinnedResults = e.target.result
 | 
					      let pinnedResults = e.target.result
 | 
				
			||||||
      let res = new Array(pinnedResults.length)
 | 
					      let res = new Array(pinnedResults.length)
 | 
				
			||||||
| 
						 | 
					@ -410,19 +423,6 @@ export async function getPinnedStatuses (instanceName, accountId) {
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// notifications by status
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function getNotificationIdsForStatus (instanceName, statusId) {
 | 
					 | 
				
			||||||
  const db = await getDatabase(instanceName)
 | 
					 | 
				
			||||||
  return dbPromise(db, NOTIFICATIONS_STORE, 'readonly', (notificationStore, callback) => {
 | 
					 | 
				
			||||||
    notificationStore.index(statusId).getAllKeys(IDBKeyRange.only(statusId)).onsuccess = e => {
 | 
					 | 
				
			||||||
      callback(Array.from(e.target.result))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
// update statuses
 | 
					// update statuses
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										7
									
								
								routes/_database/utils.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								routes/_database/utils.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					export function deleteAll (store, index, keyRange) {
 | 
				
			||||||
 | 
					  index.getAllKeys(keyRange).onsuccess = e => {
 | 
				
			||||||
 | 
					    for (let result of e.target.result) {
 | 
				
			||||||
 | 
					      store.delete(result)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,11 @@
 | 
				
			||||||
import padStart from 'lodash/padStart'
 | 
					import padStart from 'lodash/padStart'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function zeroPad (str, toSize) {
 | 
				
			||||||
 | 
					  return padStart(str, toSize, '0')
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function toPaddedBigInt (id) {
 | 
					export function toPaddedBigInt (id) {
 | 
				
			||||||
  return padStart(id, 30, '0')
 | 
					  return zeroPad(id, 30)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function toReversePaddedBigInt (id) {
 | 
					export function toReversePaddedBigInt (id) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,6 +20,11 @@ export async function postAsAdmin (text) {
 | 
				
			||||||
    null, null, false, null, 'public')
 | 
					    null, null, false, null, 'public')
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function postReplyAsAdmin (text, inReplyTo) {
 | 
				
			||||||
 | 
					  return postStatus(instanceName, users.admin.accessToken, text,
 | 
				
			||||||
 | 
					    inReplyTo, null, false, null, 'public')
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function deleteAsAdmin (statusId) {
 | 
					export async function deleteAsAdmin (statusId) {
 | 
				
			||||||
  return deleteStatus(instanceName, users.admin.accessToken, statusId)
 | 
					  return deleteStatus(instanceName, users.admin.accessToken, statusId)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,9 @@
 | 
				
			||||||
import { foobarRole } from '../roles'
 | 
					import { foobarRole } from '../roles'
 | 
				
			||||||
import { getNthStatus } from '../utils'
 | 
					import {
 | 
				
			||||||
import { deleteAsAdmin, postAsAdmin } from '../serverActions'
 | 
					  clickToNotificationsAndBackHome, forceOffline, forceOnline, getNthStatus, getUrl, homeNavButton,
 | 
				
			||||||
 | 
					  sleep
 | 
				
			||||||
 | 
					} from '../utils'
 | 
				
			||||||
 | 
					import { deleteAsAdmin, postAsAdmin, postReplyAsAdmin } from '../serverActions'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fixture`105-deletes.js`
 | 
					fixture`105-deletes.js`
 | 
				
			||||||
  .page`http://localhost:4002`
 | 
					  .page`http://localhost:4002`
 | 
				
			||||||
| 
						 | 
					@ -9,7 +12,44 @@ test('deleted statuses are removed from the timeline', async t => {
 | 
				
			||||||
  await t.useRole(foobarRole)
 | 
					  await t.useRole(foobarRole)
 | 
				
			||||||
    .hover(getNthStatus(0))
 | 
					    .hover(getNthStatus(0))
 | 
				
			||||||
  let status = await postAsAdmin("I'm gonna delete this")
 | 
					  let status = await postAsAdmin("I'm gonna delete this")
 | 
				
			||||||
 | 
					  await sleep(1000)
 | 
				
			||||||
  await t.expect(getNthStatus(0).innerText).contains("I'm gonna delete this")
 | 
					  await t.expect(getNthStatus(0).innerText).contains("I'm gonna delete this")
 | 
				
			||||||
  await deleteAsAdmin(status.id)
 | 
					  await deleteAsAdmin(status.id)
 | 
				
			||||||
 | 
					  await sleep(1000)
 | 
				
			||||||
  await t.expect(getNthStatus(0).innerText).notContains("I'm gonna delete this")
 | 
					  await t.expect(getNthStatus(0).innerText).notContains("I'm gonna delete this")
 | 
				
			||||||
 | 
					  await clickToNotificationsAndBackHome(t)
 | 
				
			||||||
 | 
					  await t.expect(getNthStatus(0).innerText).notContains("I'm gonna delete this")
 | 
				
			||||||
 | 
					  await t.navigateTo('/notifications')
 | 
				
			||||||
 | 
					  await forceOffline()
 | 
				
			||||||
 | 
					  await t.click(homeNavButton)
 | 
				
			||||||
 | 
					  await t.expect(getNthStatus(0).innerText).notContains("I'm gonna delete this")
 | 
				
			||||||
 | 
					  await forceOnline()
 | 
				
			||||||
 | 
					  await t
 | 
				
			||||||
 | 
					    .navigateTo('/')
 | 
				
			||||||
 | 
					    .expect(getNthStatus(0).innerText).notContains("I'm gonna delete this")
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('deleted statuses are removed from threads', async t => {
 | 
				
			||||||
 | 
					  await t.useRole(foobarRole)
 | 
				
			||||||
 | 
					    .hover(getNthStatus(0))
 | 
				
			||||||
 | 
					  let status = await postAsAdmin("I won't delete this")
 | 
				
			||||||
 | 
					  let reply = await postReplyAsAdmin('But I will delete this', status.id)
 | 
				
			||||||
 | 
					  await sleep(5000)
 | 
				
			||||||
 | 
					  await t.expect(getNthStatus(0).innerText).contains('But I will delete this')
 | 
				
			||||||
 | 
					    .expect(getNthStatus(1).innerText).contains("I won't delete this")
 | 
				
			||||||
 | 
					    .click(getNthStatus(1))
 | 
				
			||||||
 | 
					    .expect(getUrl()).contains('/statuses')
 | 
				
			||||||
 | 
					    .expect(getNthStatus(0).innerText).contains("I won't delete this")
 | 
				
			||||||
 | 
					    .expect(getNthStatus(1).innerText).contains('But I will delete this')
 | 
				
			||||||
 | 
					  await deleteAsAdmin(reply.id)
 | 
				
			||||||
 | 
					  await sleep(1000)
 | 
				
			||||||
 | 
					  await t.expect(getNthStatus(1).exists).notOk()
 | 
				
			||||||
 | 
					    .expect(getNthStatus(0).innerText).contains("I won't delete this")
 | 
				
			||||||
 | 
					  await t.navigateTo('/')
 | 
				
			||||||
 | 
					  await forceOffline()
 | 
				
			||||||
 | 
					  await t.click(getNthStatus(0))
 | 
				
			||||||
 | 
					    .expect(getUrl()).contains('/statuses')
 | 
				
			||||||
 | 
					    .expect(getNthStatus(1).exists).notOk()
 | 
				
			||||||
 | 
					    .expect(getNthStatus(0).innerText).contains("I won't delete this")
 | 
				
			||||||
 | 
					  await forceOnline()
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -186,3 +186,10 @@ export async function scrollToStatus (t, n) {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  await t.hover(getNthStatus(n))
 | 
					  await t.hover(getNthStatus(n))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function clickToNotificationsAndBackHome (t) {
 | 
				
			||||||
 | 
					  await t.click(notificationsNavButton)
 | 
				
			||||||
 | 
					    .expect(getUrl()).eql('http://localhost:4002/notifications')
 | 
				
			||||||
 | 
					    .click(homeNavButton)
 | 
				
			||||||
 | 
					    .expect(getUrl()).eql('http://localhost:4002/')
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue