Skip to content
This repository was archived by the owner on Dec 11, 2019. It is now read-only.

Commit 1a5e0b0

Browse files
committed
handle empty objects in sync DELETEs
fix #9308
1 parent 7b76b6a commit 1a5e0b0

File tree

6 files changed

+127
-96
lines changed

6 files changed

+127
-96
lines changed

app/browser/reducers/sitesReducer.js

+17-17
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ const filtering = require('../../filtering')
99
const siteCache = require('../../common/state/siteCache')
1010
const siteTags = require('../../../js/constants/siteTags')
1111
const siteUtil = require('../../../js/state/siteUtil')
12-
const syncActions = require('../../../js/actions/syncActions')
1312
const syncUtil = require('../../../js/state/syncUtil')
1413
const Immutable = require('immutable')
1514
const settings = require('../../../js/constants/settings')
@@ -79,8 +78,7 @@ const sitesReducer = (state, action, immutableAction) => {
7978
state = updateActiveTabBookmarked(state)
8079
break
8180
case appConstants.APP_REMOVE_SITE:
82-
const removeSiteSyncCallback = action.skipSync ? undefined : syncActions.removeSite
83-
state = siteUtil.removeSite(state, action.siteDetail, action.tag, true, removeSiteSyncCallback)
81+
state = siteUtil.removeSite(state, action.siteDetail, action.tag, true)
8482
if (syncEnabled()) {
8583
state = syncUtil.updateSiteCache(state, action.siteDetail)
8684
}
@@ -96,6 +94,7 @@ const sitesReducer = (state, action, immutableAction) => {
9694
}
9795
break
9896
case appConstants.APP_APPLY_SITE_RECORDS:
97+
// Applies site records fetched by sync
9998
let nextFolderId = siteUtil.getNextFolderId(state.get('sites'))
10099
// Ensure that all folders are assigned folderIds
101100
action.records.forEach((record, i) => {
@@ -109,21 +108,22 @@ const sitesReducer = (state, action, immutableAction) => {
109108
}
110109
})
111110
action.records.forEach((record) => {
112-
const siteData = syncUtil.getSiteDataFromRecord(record, state, action.records)
113-
const tag = siteData.tag
114-
let siteDetail = siteData.siteDetail
115-
switch (record.action) {
116-
case writeActions.CREATE:
117-
state = siteUtil.addSite(state, siteDetail, tag, undefined, true)
118-
break
119-
case writeActions.UPDATE:
120-
state = siteUtil.addSite(state, siteDetail, tag, siteData.existingObjectData, true)
121-
break
122-
case writeActions.DELETE:
123-
state = siteUtil.removeSite(state, siteDetail, tag)
124-
break
111+
if (record.action === writeActions.DELETE) {
112+
state = siteUtil.removeSiteByObjectId(state, record.objectId, record.objectData)
113+
} else {
114+
const siteData = syncUtil.getSiteDataFromRecord(record, state, action.records)
115+
const tag = siteData.tag
116+
let siteDetail = siteData.siteDetail
117+
switch (record.action) {
118+
case writeActions.CREATE:
119+
state = siteUtil.addSite(state, siteDetail, tag, undefined, true)
120+
break
121+
case writeActions.UPDATE:
122+
state = siteUtil.addSite(state, siteDetail, tag, siteData.existingObjectData, true)
123+
break
124+
}
125+
state = syncUtil.updateSiteCache(state, siteDetail)
125126
}
126-
state = syncUtil.updateSiteCache(state, siteDetail)
127127
})
128128
break
129129
case appConstants.APP_TAB_UPDATED:

app/sync.js

+55-61
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const qr = require('qr-image')
1010
const ipcMain = electron.ipcMain
1111
const locale = require('./locale')
1212
const messages = require('../js/constants/messages')
13+
const siteTags = require('../js/constants/siteTags')
1314
const syncMessages = require('../js/constants/sync/messages')
1415
const categories = require('../js/constants/sync/proto').categories
1516
const writeActions = require('../js/constants/sync/proto').actions
@@ -27,7 +28,6 @@ const extensions = require('./extensions')
2728

2829
const CATEGORY_MAP = syncUtil.CATEGORY_MAP
2930
const CATEGORY_NAMES = Object.keys(categories)
30-
const SYNC_ACTIONS = Object.values(syncConstants)
3131

3232
// The sync background script message sender
3333
let backgroundSender = null
@@ -47,7 +47,7 @@ let pollIntervalId = null
4747
let deviceIdSent = false
4848
let bookmarksToolbarShown = false
4949

50-
// Determines what to sync
50+
// Syncs state diffs to the sync server if needed
5151
const appStoreChangeCallback = function (diffs) {
5252
if (!backgroundSender) {
5353
return
@@ -73,34 +73,61 @@ const appStoreChangeCallback = function (diffs) {
7373
return
7474
}
7575

76-
const isInsert = diff.op === 'add' && path.length === 3
77-
const isUpdate = fieldsToPick.includes(path[3]) // Ignore insignicant updates
76+
let action = null
7877

79-
// DELETES are handled in appState because the old object is no longer
80-
// available by the time emitChanges is received
81-
if (isInsert || isUpdate) {
82-
// Get the item's path and entry in appStore
83-
const statePath = path.slice(1, 3).map((item) => item.replace(/~1/g, '/'))
84-
const entry = AppStore.getState().getIn(statePath)
85-
if (!entry || !entry.toJS) {
86-
return
87-
}
88-
89-
let action = null
90-
91-
if (isInsert && !entry.get('skipSync')) {
78+
if (path.length === 3 || path[3] === 'tags') {
79+
// XXX: adding/removing a tag (e.g. 'bookmark') corresponds to adding/deleting a site record in sync
80+
if (diff.op === 'add') {
9281
action = writeActions.CREATE
93-
} else if (isUpdate) {
94-
action = writeActions.UPDATE
82+
} else if (diff.op === 'remove') {
83+
action = writeActions.DELETE
9584
}
85+
} else if (fieldsToPick.includes(path[3])) {
86+
action = writeActions.UPDATE
87+
}
9688

97-
if (action !== null) {
98-
// Set the object ID if there is not already one
99-
const entryJS = entry.toJS()
100-
entryJS.objectId = entryJS.objectId || syncUtil.newObjectId(statePath)
89+
if (action === null) {
90+
return
91+
}
10192

102-
sendSyncRecords(backgroundSender, action,
103-
[type === 'sites' ? syncUtil.createSiteData(entryJS) : syncUtil.createSiteSettingsData(statePath[1], entryJS)])
93+
const statePath = path.slice(1, 3).map((item) => item.replace(/~1/g, '/'))
94+
const state = AppStore.getState()
95+
const entry = state.getIn(statePath)
96+
const isSite = type === 'sites'
97+
98+
if (isSite && action === writeActions.DELETE && !entry) {
99+
// If we deleted the site, it is no longer availble in appState.
100+
// Find the corresponding objectId using the sync cache
101+
// and send this in the sync record
102+
const objectId = syncUtil.siteKeyToObjectId(state, statePath[1])
103+
if (objectId) {
104+
// Delete the site from both history and bookmarks
105+
sendSyncRecords(backgroundSender, action, [{
106+
name: 'bookmark',
107+
objectId,
108+
value: {}
109+
}])
110+
sendSyncRecords(backgroundSender, action, [{
111+
name: 'historySite',
112+
objectId,
113+
value: {}
114+
}])
115+
}
116+
} else if (entry && entry.toJS) {
117+
const entryJS = entry.toJS()
118+
if (action === writeActions.DELETE && isSite) {
119+
const tags = entryJS.tags || []
120+
sendSyncRecords(backgroundSender, action, [{
121+
objectId: entryJS.objectId,
122+
value: {},
123+
name: tags.includes(siteTags.BOOKMARK) || siteTags.includes(siteTags.BOOKMARK_FOLDER)
124+
? 'historySite' // if the site is still a bookmark, it must have been deleted from history
125+
: 'bookmark'
126+
}])
127+
} else {
128+
sendSyncRecords(backgroundSender, action, [
129+
isSite ? syncUtil.createSiteData(entryJS, state) : syncUtil.createSiteSettingsData(statePath[1], entryJS)
130+
])
104131
}
105132
}
106133
})
@@ -110,7 +137,7 @@ const appStoreChangeCallback = function (diffs) {
110137
* Sends sync records of the same category to the sync server.
111138
* @param {event.sender} sender
112139
* @param {number} action
113-
* @param {Array.<{name: string, value: Object}>} data
140+
* @param {Array.<{objectId: Array, name: string, value: Object}>} data
114141
*/
115142
const sendSyncRecords = (sender, action, data) => {
116143
if (!deviceId) {
@@ -125,7 +152,7 @@ const sendSyncRecords = (sender, action, data) => {
125152
return
126153
}
127154
sender.send(syncMessages.SEND_SYNC_RECORDS, category.categoryName, data.map((item) => {
128-
if (!item || !item.name || !item.value) {
155+
if (!item || !item.name || !item.value || !item.objectId) {
129156
return
130157
}
131158
return {
@@ -137,34 +164,6 @@ const sendSyncRecords = (sender, action, data) => {
137164
}))
138165
}
139166

140-
/**
141-
* @param {Object} action
142-
* @returns {boolean}
143-
*/
144-
const validateAction = (action) => {
145-
const SYNC_ACTIONS_WITHOUT_ITEMS = [
146-
syncConstants.SYNC_CLEAR_HISTORY,
147-
syncConstants.SYNC_CLEAR_SITE_SETTINGS
148-
]
149-
if (SYNC_ACTIONS.includes(action.actionType) !== true) {
150-
return false
151-
}
152-
153-
// If the action requires an item, validate the item.
154-
if (SYNC_ACTIONS_WITHOUT_ITEMS.includes(action.actionType) !== true) {
155-
if (!action.item || !action.item.toJS) {
156-
log('Missing item!')
157-
return false
158-
}
159-
// Only accept items who have an objectId set already
160-
if (!action.item.get('objectId')) {
161-
log(`Missing object ID! ${action.item.toJS()}`)
162-
return false
163-
}
164-
}
165-
return true
166-
}
167-
168167
const dispatcherCallback = (action) => {
169168
if (!backgroundSender) {
170169
return
@@ -176,14 +175,10 @@ const dispatcherCallback = (action) => {
176175
}
177176
}
178177
// If sync is not enabled, the following actions should be ignored.
179-
if (!syncEnabled() || validateAction(action) !== true || backgroundSender.isDestroyed()) {
178+
if (!syncEnabled() || backgroundSender.isDestroyed()) {
180179
return
181180
}
182181
switch (action.actionType) {
183-
case syncConstants.SYNC_REMOVE_SITE:
184-
sendSyncRecords(backgroundSender, writeActions.DELETE,
185-
[syncUtil.createSiteData(action.item.toJS())])
186-
break
187182
case syncConstants.SYNC_CLEAR_HISTORY:
188183
backgroundSender.send(syncMessages.DELETE_SYNC_CATEGORY, CATEGORY_MAP.historySite.categoryName)
189184
break
@@ -318,7 +313,6 @@ module.exports.init = function (appState) {
318313
}
319314
// sent by about:preferences when sync should be reloaded
320315
ipcMain.on(messages.RELOAD_SYNC_EXTENSION, () => {
321-
console.log('reloading sync')
322316
extensions.reloadExtension(syncExtensionId)
323317
})
324318
// sent by about:preferences when resetting sync

js/actions/syncActions.js

-7
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,6 @@ const AppDispatcher = require('../dispatcher/appDispatcher')
77
const syncConstants = require('../constants/syncConstants')
88

99
const syncActions = {
10-
removeSite: function (item) {
11-
AppDispatcher.dispatch({
12-
actionType: syncConstants.SYNC_REMOVE_SITE,
13-
item
14-
})
15-
},
16-
1710
clearHistory: function () {
1811
AppDispatcher.dispatch({
1912
actionType: syncConstants.SYNC_CLEAR_HISTORY

js/constants/syncConstants.js

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ const mapValuesByKeys = require('../lib/functional').mapValuesByKeys
66

77
const _ = null
88
const syncConstants = {
9-
SYNC_REMOVE_SITE: _, /** @param {Immutable.Map} item */
109
SYNC_CLEAR_HISTORY: _,
1110
SYNC_CLEAR_SITE_SETTINGS: _
1211
}

js/state/siteUtil.js

+33-6
Original file line numberDiff line numberDiff line change
@@ -331,25 +331,52 @@ module.exports.addSite = function (state, siteDetail, tag, originalSiteDetail, s
331331
return state
332332
}
333333

334+
/**
335+
* Removes the appropriate tag from a site given the site's sync objectId.
336+
* @param {Immutable.Map} state - Application state
337+
* @param {Array} objectId - Object ID of object to be deleted
338+
* @param {string} objectData - oneof 'historySite', 'bookmark'
339+
* @returns {Immutable.Map}
340+
*/
341+
module.exports.removeSiteByObjectId = function (state, objectId, objectData) {
342+
if (!objectId) {
343+
return state
344+
}
345+
const cacheKey = ['sync', 'objectsById', objectId.join('|')]
346+
const objectKey = state.getIn(cacheKey)
347+
const site = objectKey && state.getIn(objectKey)
348+
if (!site) {
349+
return state
350+
}
351+
let tag = ''
352+
if (objectData === 'bookmark') {
353+
// determine whether this is a folder
354+
tag = site.get('folderId') ? siteTags.BOOKMARK_FOLDER : siteTags.BOOKMARK
355+
}
356+
state = module.exports.removeSite(state, site, tag)
357+
// If the object has been removed completely, purge it from the sync site
358+
// cache
359+
if (!state.getIn(objectKey)) {
360+
state = state.deleteIn(cacheKey)
361+
}
362+
return state
363+
}
364+
334365
/**
335366
* Removes the specified tag from a siteDetail
336367
*
337368
* @param {Immutable.Map} state The application state Immutable map
338369
* @param {Immutable.Map} siteDetail The siteDetail to remove a tag from
339370
* @param {string} tag
340371
* @param {boolean} reorder whether to reorder sites (default with reorder)
341-
* @param {Function=} syncCallback
342372
* @return {Immutable.Map} The new state Immutable object
343373
*/
344-
module.exports.removeSite = function (state, siteDetail, tag, reorder = true, syncCallback) {
374+
module.exports.removeSite = function (state, siteDetail, tag, reorder = true) {
345375
let sites = state.get('sites')
346376
const key = module.exports.getSiteKey(siteDetail)
347377
if (!key) {
348378
return state
349379
}
350-
if (getSetting(settings.SYNC_ENABLED) === true && syncCallback) {
351-
syncCallback(sites.getIn([key]))
352-
}
353380

354381
const tags = sites.getIn([key, 'tags'])
355382
if (isBookmarkFolder(tags)) {
@@ -358,7 +385,7 @@ module.exports.removeSite = function (state, siteDetail, tag, reorder = true, sy
358385
childSites.forEach((site) => {
359386
const tags = site.get('tags')
360387
tags.forEach((tag) => {
361-
state = module.exports.removeSite(state, site, tag, false, syncCallback)
388+
state = module.exports.removeSite(state, site, tag, false)
362389
})
363390
})
364391
}

js/state/syncUtil.js

+22-4
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,6 @@ module.exports.getExistingObject = (categoryName, syncRecord) => {
258258
throw new Error(`Invalid category: ${categoryName}`)
259259
}
260260
if (!item) {
261-
console.log(`Warning: Can't create JS from existingObject! ${JSON.stringify(existingObjectData)}`)
262261
return null
263262
}
264263
return {
@@ -305,8 +304,27 @@ module.exports.updateSiteCache = (appState, siteDetail) => {
305304
const cacheKey = ['sync', 'objectsById', objectId.toJS().join('|')]
306305
if (object) {
307306
return appState.setIn(cacheKey, ['sites', siteKey])
308-
} else {
309-
return appState.deleteIn(cacheKey)
307+
}
308+
// Keep obsolete object keys in the cache because they are used to get
309+
// objectIds of deleted objects in appStoreChangeCallback
310+
}
311+
312+
/**
313+
* Gets the objectId of a site based on its siteKey.
314+
* Does not assume the site hasn't been deleted.
315+
* @param {Immutable.Map} appState - application state
316+
* @param {string} siteKey - The state path, or undefined if none is found
317+
* @returns {Array=}
318+
*/
319+
module.exports.siteKeyToObjectId = (appState, siteKey) => {
320+
const objectsById = appState.getIn(['sync', 'objectsById'])
321+
if (objectsById) {
322+
const objectIdKey = objectsById.findKey((value, key) => {
323+
return value[0] === 'sites' && value[1] === siteKey
324+
})
325+
if (objectIdKey) {
326+
return objectIdKey.split('|')
327+
}
310328
}
311329
}
312330

@@ -485,7 +503,7 @@ module.exports.createSiteData = (site, appState) => {
485503
value: siteData
486504
}
487505
}
488-
console.log(`Warning: Can't create site data: ${JSON.stringify(site)}`)
506+
console.log(`Ignoring entry because we can't create site data: ${JSON.stringify(site)}`)
489507
}
490508

491509
/**

0 commit comments

Comments
 (0)