basic support for delete streaming

This commit is contained in:
Nolan Lawson 2018-03-10 16:21:10 -08:00
parent d4e48ac6fa
commit 23a247a8c2
10 changed files with 102 additions and 31 deletions

View File

@ -38,7 +38,7 @@ async function insertUpdatesIntoThreads (instanceName, updates) {
return
}
let threads = store.getThreadsForTimeline(instanceName)
let threads = store.getThreads(instanceName)
for (let timelineName of Object.keys(threads)) {
let thread = threads[timelineName]

View File

@ -1,21 +1,28 @@
import { getIdsThatRebloggedThisStatus, getIdThatThisStatusReblogged, getNotificationIdsForStatuses } from './statuses'
import { getIdsThatRebloggedThisStatus, getNotificationIdsForStatuses } from './statuses'
import { store } from '../_store/store'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import { database } from '../_database/database'
import forEach from 'lodash/forEach'
function deleteStatusIdsFromStore (instanceName, idsToDelete) {
let idsToDeleteSet = new Set(idsToDelete)
let timelines = store.get('timelines')
if (timelines && timelines[instanceName]) {
Object.keys(timelines[instanceName]).forEach(timelineName => {
let timelineData = timelines[instanceName][timelineName]
if (timelineName !== 'notifications') {
timelineData.timelineItemIds = timelineData.timelineItemIds.filter(_ => !idsToDeleteSet.has(_))
timelineData.itemIdsToAdd = timelineData.itemIdsToAdd.filter(_ => !idsToDeleteSet.has(_))
}
let idWasNotDeleted = id => !idsToDeleteSet.has(id)
let timelinesToTimelineItemIds = store.getAllTimelineData(instanceName, 'timelineItemIds')
forEach(timelinesToTimelineItemIds, (timelineItemIds, timelineName) => {
store.setForTimeline(instanceName, timelineName, {
timelineItemIds: timelineItemIds.filter(idWasNotDeleted)
})
})
let timelinesToItemIdsToAdd = store.getAllTimelineData(instanceName, 'itemIdsToAdd')
forEach(timelinesToItemIdsToAdd, (itemIdsToAdd, timelineName) => {
store.setForTimeline(instanceName, timelineName, {
itemIdsToAdd: itemIdsToAdd.filter(idWasNotDeleted)
})
})
store.set({timelines: timelines})
}
}
async function deleteStatusesAndNotifications (instanceName, statusIdsToDelete, notificationIdsToDelete) {
@ -24,9 +31,9 @@ async function deleteStatusesAndNotifications (instanceName, statusIdsToDelete,
}
async function doDeleteStatus (instanceName, statusId) {
let reblogId = await getIdThatThisStatusReblogged(instanceName, statusId)
let rebloggedIds = await getIdsThatRebloggedThisStatus(reblogId || statusId)
let statusIdsToDelete = Array.from(new Set([statusId, reblogId].concat(rebloggedIds).filter(Boolean)))
console.log('deleting statusId', statusId)
let rebloggedIds = await getIdsThatRebloggedThisStatus(instanceName, statusId)
let statusIdsToDelete = Array.from(new Set([statusId].concat(rebloggedIds).filter(Boolean)))
let notificationIdsToDelete = new Set(await getNotificationIdsForStatuses(instanceName, statusIdsToDelete))
await Promise.all([
deleteStatusesAndNotifications(instanceName, statusIdsToDelete, notificationIdsToDelete)

View File

@ -7,16 +7,15 @@ import { addStatusOrNotification } from './addStatusOrNotification'
function processMessage (instanceName, timelineName, message) {
mark('processMessage')
let { event, payload } = message
let parsedPayload = JSON.parse(payload)
switch (event) {
case 'delete':
deleteStatus(instanceName, parsedPayload)
deleteStatus(instanceName, payload)
break
case 'update':
addStatusOrNotification(instanceName, timelineName, parsedPayload)
addStatusOrNotification(instanceName, timelineName, JSON.parse(payload))
break
case 'notification':
addStatusOrNotification(instanceName, 'notifications', parsedPayload)
addStatusOrNotification(instanceName, 'notifications', JSON.parse(payload))
break
}
stop('processMessage')

7
routes/_api/delete.js Normal file
View File

@ -0,0 +1,7 @@
import { auth, basename } from './utils'
import { deleteWithTimeout } from '../_utils/ajax'
export async function deleteStatus (instanceName, accessToken, statusId) {
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}`
return deleteWithTimeout(url, auth(accessToken))
}

View File

@ -9,13 +9,14 @@ import {
PINNED_STATUSES_STORE,
TIMESTAMP,
REBLOG_ID,
THREADS_STORE
THREADS_STORE,
STATUS_ID
} from './constants'
const openReqs = {}
const databaseCache = {}
const DB_VERSION = 5
const DB_VERSION = 6
export function getDatabase (instanceName) {
if (!instanceName) {
@ -54,6 +55,9 @@ export function getDatabase (instanceName) {
tx.objectStore(NOTIFICATIONS_STORE).createIndex('statusId', 'statusId')
db.createObjectStore(THREADS_STORE)
}
if (e.oldVersion < 6) {
tx.objectStore(NOTIFICATIONS_STORE).createIndex(STATUS_ID, STATUS_ID)
}
}
req.onsuccess = () => resolve(req.result)
})

View File

@ -288,6 +288,24 @@ export async function getReblogsForStatus (instanceName, id) {
})
}
//
// lookups by statusId
//
export async function getNotificationIdsForStatuses (instanceName, statusIds) {
const db = await getDatabase(instanceName)
await dbPromise(db, NOTIFICATIONS_STORE, 'readonly', (notificationsStore, callback) => {
let res = []
callback(res)
statusIds.forEach(statusId => {
let req = notificationsStore.index(STATUS_ID).getAllKeys(IDBKeyRange.only(statusId))
req.onsuccess = e => {
res = res.concat(e.target.result)
}
})
})
}
//
// deletes
//

View File

@ -20,23 +20,22 @@ export function timelineMixins (Store) {
return root && root[instanceName] && root[instanceName][timelineName]
}
Store.prototype.getAllTimelineData = function (instanceName, key) {
let root = this.get(`timelineData_${key}`) || {}
return root[instanceName] || {}
}
Store.prototype.setForCurrentTimeline = function (obj) {
let instanceName = this.get('currentInstance')
let timelineName = this.get('currentTimeline')
this.setForTimeline(instanceName, timelineName, obj)
}
Store.prototype.getThreadsForTimeline = function (instanceName) {
let root = this.get('timelineData_timelineItemIds') || {}
let instanceData = root[instanceName] = root[instanceName] || {}
Store.prototype.getThreads = function (instanceName) {
let instanceData = this.getAllTimelineData(instanceName, 'timelineItemIds')
return pickBy(instanceData, (value, key) => {
return key.startsWith('status/')
})
}
Store.prototype.getThreadsForCurrentTimeline = function () {
let instanceName = this.get('currentInstance')
return this.getThreadsForTimeline(instanceName)
}
}

View File

@ -52,6 +52,17 @@ async function _get (url, headers, timeout) {
return throwErrorIfInvalidResponse(response)
}
async function _delete (url, headers, timeout) {
let fetchFunc = timeout ? fetchWithTimeout : fetch
let response = await fetchFunc(url, {
method: 'DELETE',
headers: Object.assign(headers, {
'Accept': 'application/json'
})
})
return throwErrorIfInvalidResponse(response)
}
export async function post (url, body, headers = {}) {
return _post(url, body, headers, false)
}
@ -68,6 +79,10 @@ export async function get (url, headers = {}) {
return _get(url, headers, false)
}
export async function deleteWithTimeout (url, headers = {}) {
return _delete(url, headers, true)
}
export function paramsString (paramsObject) {
let res = ''
Object.keys(paramsObject).forEach((key, i) => {

View File

@ -3,16 +3,23 @@ import fetch from 'node-fetch'
import FileApi from 'file-api'
import { users } from './users'
import { postStatus } from '../routes/_api/statuses'
import { deleteStatus } from '../routes/_api/delete'
global.fetch = fetch
global.File = FileApi.File
global.FormData = FileApi.FormData
const instanceName = 'localhost:3000'
export async function favoriteStatusAsAdmin (statusId) {
return favoriteStatus('localhost:3000', users.admin.accessToken, statusId)
return favoriteStatus(instanceName, users.admin.accessToken, statusId)
}
export async function postAsAdmin (text) {
return postStatus('localhost:3000', users.admin.accessToken, text,
return postStatus(instanceName, users.admin.accessToken, text,
null, null, false, null, 'public')
}
export async function deleteAsAdmin (statusId) {
return deleteStatus(instanceName, users.admin.accessToken, statusId)
}

15
tests/spec/105-deletes.js Normal file
View File

@ -0,0 +1,15 @@
import { foobarRole } from '../roles'
import { getNthStatus } from '../utils'
import { deleteAsAdmin, postAsAdmin } from '../serverActions'
fixture`105-deletes.js`
.page`http://localhost:4002`
test('deleted statuses are removed from the timeline', async t => {
await t.useRole(foobarRole)
.hover(getNthStatus(0))
let status = await postAsAdmin("I'm gonna delete this")
await t.expect(getNthStatus(0).innerText).contains("I'm gonna delete this")
await deleteAsAdmin(status.id)
await t.expect(getNthStatus(0).innerText).notContains("I'm gonna delete this")
})