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

View file

@ -7,16 +7,15 @@ import { addStatusOrNotification } from './addStatusOrNotification'
function processMessage (instanceName, timelineName, message) { function processMessage (instanceName, timelineName, message) {
mark('processMessage') mark('processMessage')
let { event, payload } = message let { event, payload } = message
let parsedPayload = JSON.parse(payload)
switch (event) { switch (event) {
case 'delete': case 'delete':
deleteStatus(instanceName, parsedPayload) deleteStatus(instanceName, payload)
break break
case 'update': case 'update':
addStatusOrNotification(instanceName, timelineName, parsedPayload) addStatusOrNotification(instanceName, timelineName, JSON.parse(payload))
break break
case 'notification': case 'notification':
addStatusOrNotification(instanceName, 'notifications', parsedPayload) addStatusOrNotification(instanceName, 'notifications', JSON.parse(payload))
break break
} }
stop('processMessage') 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, PINNED_STATUSES_STORE,
TIMESTAMP, TIMESTAMP,
REBLOG_ID, REBLOG_ID,
THREADS_STORE THREADS_STORE,
STATUS_ID
} from './constants' } from './constants'
const openReqs = {} const openReqs = {}
const databaseCache = {} const databaseCache = {}
const DB_VERSION = 5 const DB_VERSION = 6
export function getDatabase (instanceName) { export function getDatabase (instanceName) {
if (!instanceName) { if (!instanceName) {
@ -54,6 +55,9 @@ export function getDatabase (instanceName) {
tx.objectStore(NOTIFICATIONS_STORE).createIndex('statusId', 'statusId') tx.objectStore(NOTIFICATIONS_STORE).createIndex('statusId', 'statusId')
db.createObjectStore(THREADS_STORE) db.createObjectStore(THREADS_STORE)
} }
if (e.oldVersion < 6) {
tx.objectStore(NOTIFICATIONS_STORE).createIndex(STATUS_ID, STATUS_ID)
}
} }
req.onsuccess = () => resolve(req.result) 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 // deletes
// //

View file

@ -20,23 +20,22 @@ export function timelineMixins (Store) {
return root && root[instanceName] && root[instanceName][timelineName] 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) { Store.prototype.setForCurrentTimeline = function (obj) {
let instanceName = this.get('currentInstance') let instanceName = this.get('currentInstance')
let timelineName = this.get('currentTimeline') let timelineName = this.get('currentTimeline')
this.setForTimeline(instanceName, timelineName, obj) this.setForTimeline(instanceName, timelineName, obj)
} }
Store.prototype.getThreadsForTimeline = function (instanceName) { Store.prototype.getThreads = function (instanceName) {
let root = this.get('timelineData_timelineItemIds') || {} let instanceData = this.getAllTimelineData(instanceName, 'timelineItemIds')
let instanceData = root[instanceName] = root[instanceName] || {}
return pickBy(instanceData, (value, key) => { return pickBy(instanceData, (value, key) => {
return key.startsWith('status/') 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) 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 = {}) { export async function post (url, body, headers = {}) {
return _post(url, body, headers, false) return _post(url, body, headers, false)
} }
@ -68,6 +79,10 @@ export async function get (url, headers = {}) {
return _get(url, headers, false) return _get(url, headers, false)
} }
export async function deleteWithTimeout (url, headers = {}) {
return _delete(url, headers, true)
}
export function paramsString (paramsObject) { export function paramsString (paramsObject) {
let res = '' let res = ''
Object.keys(paramsObject).forEach((key, i) => { Object.keys(paramsObject).forEach((key, i) => {

View file

@ -3,16 +3,23 @@ import fetch from 'node-fetch'
import FileApi from 'file-api' import FileApi from 'file-api'
import { users } from './users' import { users } from './users'
import { postStatus } from '../routes/_api/statuses' import { postStatus } from '../routes/_api/statuses'
import { deleteStatus } from '../routes/_api/delete'
global.fetch = fetch global.fetch = fetch
global.File = FileApi.File global.File = FileApi.File
global.FormData = FileApi.FormData global.FormData = FileApi.FormData
const instanceName = 'localhost:3000'
export async function favoriteStatusAsAdmin (statusId) { 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) { 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') 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")
})