basic support for delete streaming
This commit is contained in:
		
							parent
							
								
									d4e48ac6fa
								
							
						
					
					
						commit
						23a247a8c2
					
				
					 10 changed files with 102 additions and 31 deletions
				
			
		|  | @ -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] | ||||
|  |  | |||
|  | @ -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) | ||||
|     }) | ||||
|     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) { | ||||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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
									
								
							
							
						
						
									
										7
									
								
								routes/_api/delete.js
									
										
									
									
									
										Normal 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)) | ||||
| } | ||||
|  | @ -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) | ||||
|   }) | ||||
|  |  | |||
|  | @ -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
 | ||||
| //
 | ||||
|  |  | |||
|  | @ -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) | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -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) => { | ||||
|  |  | |||
|  | @ -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
									
								
							
							
						
						
									
										15
									
								
								tests/spec/105-deletes.js
									
										
									
									
									
										Normal 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") | ||||
| }) | ||||
		Loading…
	
	Add table
		
		Reference in a new issue