Skip to content

Commit f809eb3

Browse files
authored
Merge pull request #1919 from floccusaddon/fix/changes-while-syncing
test(sync): Making changes to the tree while syncing should not cause trouble
2 parents 742bf34 + 6245264 commit f809eb3

File tree

19 files changed

+451
-149
lines changed

19 files changed

+451
-149
lines changed

src/lib/Account.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { isTest } from './isTest'
1414
import CachingAdapter from './adapters/Caching'
1515
import * as Sentry from '@sentry/vue'
1616
import AsyncLock from 'async-lock'
17+
import CachingTreeWrapper from './CachingTreeWrapper'
1718

1819
declare const DEBUG: boolean
1920

@@ -163,7 +164,7 @@ export default class Account {
163164
try {
164165
if (this.getData().syncing || this.syncing) return
165166

166-
const localResource = await this.getResource()
167+
const localResource = new CachingTreeWrapper(await this.getResource())
167168
if (!(await this.server.isAvailable()) || !(await localResource.isAvailable())) return
168169

169170
Logger.log('Starting sync process for account ' + this.getLabel())
@@ -283,9 +284,8 @@ export default class Account {
283284
await this.setData({ scheduled: false, syncing: 1 })
284285

285286
// update cache
286-
const cache = (await localResource.getBookmarksTree()).clone(false)
287+
const cache = (await localResource.getCacheTree()).clone(false)
287288
this.syncProcess.filterOutUnacceptedBookmarks(cache)
288-
this.syncProcess.filterOutUnmappedItems(cache, await mappings.getSnapshot())
289289
await this.storage.setCache(cache)
290290

291291
if (this.server.onSyncComplete) {

src/lib/CacheTree.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import CachingAdapter from './adapters/Caching'
2+
import { IResource } from './interfaces/Resource'
3+
import { Folder, ItemLocation, TItemLocation } from './Tree'
4+
5+
export default class CacheTree extends CachingAdapter implements IResource<typeof ItemLocation.LOCAL> {
6+
protected location: TItemLocation = ItemLocation.LOCAL
7+
8+
constructor() {
9+
super({})
10+
}
11+
12+
public setTree(tree: Folder<typeof ItemLocation.LOCAL>) {
13+
this.bookmarksCache = tree.clone(false)
14+
this.bookmarksCache.createIndex()
15+
}
16+
17+
async getBookmarksTree(): Promise<Folder<typeof ItemLocation.LOCAL>> {
18+
const tree = await super.getBookmarksTree()
19+
tree.createIndex()
20+
return tree as Folder<typeof ItemLocation.LOCAL>
21+
}
22+
23+
isAvailable(): Promise<boolean> {
24+
return Promise.resolve(true)
25+
}
26+
}

src/lib/CachingTreeWrapper.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { CachingResource, OrderFolderResource } from './interfaces/Resource'
2+
import { Bookmark, Folder, ItemLocation } from './Tree'
3+
import CacheTree from './CacheTree'
4+
import Ordering from './interfaces/Ordering'
5+
6+
export default class CachingTreeWrapper implements OrderFolderResource<typeof ItemLocation.LOCAL>, CachingResource<typeof ItemLocation.LOCAL> {
7+
private innerTree: OrderFolderResource<typeof ItemLocation.LOCAL>
8+
private cacheTree: CacheTree
9+
10+
constructor(innerTree: OrderFolderResource<typeof ItemLocation.LOCAL>) {
11+
this.innerTree = innerTree
12+
this.cacheTree = new CacheTree()
13+
}
14+
15+
async getBookmarksTree(): Promise<Folder<typeof ItemLocation.LOCAL>> {
16+
const tree = await this.innerTree.getBookmarksTree()
17+
this.cacheTree.setTree(tree)
18+
return tree
19+
}
20+
21+
async createBookmark(bookmark:Bookmark<typeof ItemLocation.LOCAL>): Promise<string|number> {
22+
const id = await this.innerTree.createBookmark(bookmark)
23+
const cacheId = await this.cacheTree.createBookmark(bookmark.copy(false))
24+
const cacheBookmark = this.cacheTree.bookmarksCache.findBookmark(cacheId)
25+
cacheBookmark.id = id
26+
cacheBookmark.parentId = bookmark.parentId
27+
this.cacheTree.bookmarksCache.createIndex()
28+
return id
29+
}
30+
31+
async updateBookmark(bookmark:Bookmark<typeof ItemLocation.LOCAL>):Promise<void> {
32+
await this.innerTree.updateBookmark(bookmark)
33+
await this.cacheTree.updateBookmark(bookmark.copy(false))
34+
}
35+
36+
async removeBookmark(bookmark:Bookmark<typeof ItemLocation.LOCAL>): Promise<void> {
37+
await this.innerTree.removeBookmark(bookmark)
38+
await this.cacheTree.removeBookmark(bookmark)
39+
}
40+
41+
async createFolder(folder:Folder<typeof ItemLocation.LOCAL>): Promise<string|number> {
42+
const id = await this.innerTree.createFolder(folder)
43+
const cacheId = await this.cacheTree.createFolder(folder.copy(false))
44+
const cacheFolder = this.cacheTree.bookmarksCache.findFolder(cacheId)
45+
cacheFolder.id = id
46+
cacheFolder.parentId = folder.parentId
47+
this.cacheTree.bookmarksCache.createIndex()
48+
return id
49+
}
50+
51+
async orderFolder(id:string|number, order:Ordering<typeof ItemLocation.LOCAL>): Promise<void> {
52+
await this.innerTree.orderFolder(id, order)
53+
await this.cacheTree.orderFolder(id, order)
54+
}
55+
56+
async updateFolder(folder:Folder<typeof ItemLocation.LOCAL>): Promise<void> {
57+
await this.innerTree.updateFolder(folder)
58+
await this.cacheTree.updateFolder(folder.copy(false))
59+
}
60+
61+
async removeFolder(folder:Folder<typeof ItemLocation.LOCAL>): Promise<void> {
62+
await this.innerTree.removeFolder(folder)
63+
await this.cacheTree.removeFolder(folder)
64+
}
65+
66+
isAvailable(): Promise<boolean> {
67+
return this.innerTree.isAvailable()
68+
}
69+
70+
async isUsingBrowserTabs() {
71+
return this.innerTree.isUsingBrowserTabs?.()
72+
}
73+
74+
getCacheTree(): Promise<Folder<typeof ItemLocation.LOCAL>> {
75+
return this.cacheTree.getBookmarksTree()
76+
}
77+
}

src/lib/Diff.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,16 +200,16 @@ export default class Diff<L1 extends TItemLocation, L2 extends TItemLocation, A
200200
const newId = action.payload.id
201201
newAction = {
202202
...action,
203-
payload: action.payload.cloneWithLocation(false, targetLocation),
204-
oldItem: action.oldItem.cloneWithLocation(false, action.payload.location)
203+
payload: action.payload.copyWithLocation(false, targetLocation),
204+
oldItem: action.oldItem.copyWithLocation(false, action.payload.location)
205205
}
206206
newAction.payload.id = oldId
207207
newAction.oldItem.id = newId
208208
} else {
209209
newAction = {
210210
...action,
211-
payload: action.payload.cloneWithLocation(false, targetLocation),
212-
oldItem: action.payload.clone(false)
211+
payload: action.payload.copyWithLocation(false, targetLocation),
212+
oldItem: action.payload.copy(false)
213213
}
214214
newAction.payload.id = Mappings.mapId(mappingsSnapshot, action.payload, targetLocation)
215215
}
@@ -253,8 +253,8 @@ export default class Diff<L1 extends TItemLocation, L2 extends TItemLocation, A
253253
return this.getActions().map((action: A) => {
254254
return {
255255
...action,
256-
payload: action.payload.clone(false),
257-
oldItem: action.oldItem && action.oldItem.clone(false),
256+
payload: action.payload.copy(false),
257+
oldItem: action.oldItem && action.oldItem.copy(false),
258258
}
259259
})
260260
}

src/lib/LocalTabs.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ export default class LocalTabs implements OrderFolderResource<typeof ItemLocatio
153153
})
154154
return Boolean(tabs.length)
155155
}
156+
157+
async isUsingBrowserTabs() {
158+
return true
159+
}
156160
}
157161

158162
function awaitTabsUpdated() {

src/lib/Scanner.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ export default class Scanner<L1 extends TItemLocation, L2 extends TItemLocation>
203203
this.result.REMOVE.retract(removeAction)
204204
} else {
205205
// We clone the item here, because we don't want to mutate all copies of this tree (item)
206-
const removedItemClone = removedItem.clone(true)
206+
const removedItemClone = removedItem.copy(true)
207207
const oldParentClone = removedItemClone.findItem(ItemType.FOLDER, oldItem.parentId) as Folder<L1>
208208
const oldItemClone = removedItemClone.findItem(oldItem.type, oldItem.id)
209209
oldIndex = oldParentClone.children.indexOf(oldItemClone)
@@ -235,7 +235,7 @@ export default class Scanner<L1 extends TItemLocation, L2 extends TItemLocation>
235235
this.result.CREATE.retract(createAction)
236236
} else {
237237
// We clone the item here, because we don't want to mutate all copies of this tree (item)
238-
const createdItemClone = createdItem.clone(true)
238+
const createdItemClone = createdItem.copy(true)
239239
const newParentClone = createdItemClone.findItem(ItemType.FOLDER, newItem.parentId) as Folder<L2>
240240
const newClonedItem = createdItemClone.findItem(newItem.type, newItem.id)
241241
index = newParentClone.children.indexOf(newClonedItem)

src/lib/Tree.ts

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,49 @@ export class Bookmark<L extends TItemLocation> {
8888
}
8989

9090
clone(withHash?: boolean):Bookmark<L> {
91-
return new Bookmark(this)
91+
const bookmark = Object.create(this)
92+
if (!withHash) {
93+
bookmark.hashValue = {}
94+
}
95+
return bookmark
9296
}
9397

9498
cloneWithLocation<L2 extends TItemLocation>(withHash:boolean, location: L2): Bookmark<L2> {
99+
const newBookmark = Object.create(this)
100+
newBookmark.location = location
101+
return newBookmark
102+
}
103+
104+
copy(withHash?: boolean):Bookmark<L> {
105+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
106+
// @ts-ignore
107+
return new Bookmark(this.toJSON())
108+
}
109+
110+
copyWithLocation<L2 extends TItemLocation>(withHash:boolean, location: L2): Bookmark<L2> {
111+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
112+
// @ts-ignore
95113
return new Bookmark({
96-
...this,
114+
...this.toJSON(),
97115
location,
98116
})
99117
}
100118

119+
toJSON() {
120+
// Flatten inherited properties for serialization
121+
const result = {}
122+
let obj = this
123+
while (obj) {
124+
Object.entries(obj).forEach(([key, value]) => {
125+
if (!(key in result)) {
126+
result[key] = value
127+
}
128+
})
129+
obj = Object.getPrototypeOf(obj)
130+
}
131+
return result
132+
}
133+
101134
createIndex():any {
102135
return { [this.id]: this }
103136
}
@@ -309,23 +342,64 @@ export class Folder<L extends TItemLocation> {
309342
return this.hashValue[String(preserveOrder)]
310343
}
311344

312-
clone(withHash?:boolean):Folder<L> {
345+
copy(withHash?:boolean):Folder<L> {
346+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
347+
// @ts-ignore
313348
return new Folder({
314-
...this,
349+
...this.toJSON(),
315350
...(!withHash && { hashValue: {} }),
316-
children: this.children.map(child => child.clone(withHash))
351+
children: this.children.map(child => child.copy(withHash))
317352
})
318353
}
319354

320-
cloneWithLocation<L2 extends TItemLocation>(withHash:boolean, location: L2):Folder<L2> {
355+
copyWithLocation<L2 extends TItemLocation>(withHash:boolean, location: L2):Folder<L2> {
356+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
357+
// @ts-ignore
321358
return new Folder({
322-
...this,
359+
...this.toJSON(),
323360
location,
324361
...(!withHash && { hashValue: {} }),
325-
children: this.children.map(child => child.cloneWithLocation(withHash, location))
362+
children: this.children.map(child => child.copyWithLocation(withHash, location))
326363
})
327364
}
328365

366+
clone(withHash?:boolean):Folder<L> {
367+
const newFolder = Object.create(this)
368+
newFolder.index = null
369+
if (!withHash) {
370+
newFolder.hashValue = {}
371+
}
372+
newFolder.children = this.children.map(child => child.clone(withHash))
373+
return newFolder
374+
}
375+
376+
cloneWithLocation<L2 extends TItemLocation>(withHash:boolean, location: L2):Folder<L2> {
377+
const newFolder = Object.create(this)
378+
if (!withHash) {
379+
newFolder.hashValue = {}
380+
}
381+
newFolder.index = null
382+
newFolder.location = location
383+
newFolder.children = this.children.map(child => child.cloneWithLocation(withHash, location))
384+
return newFolder
385+
}
386+
387+
toJSON(): Folder<L> {
388+
// Flatten inherited properties for serialization
389+
const result: Folder<L> = {} as any as Folder<L>
390+
let obj = this
391+
while (obj) {
392+
Object.entries(obj).forEach(([key, value]) => {
393+
if (key === 'index') return
394+
if (!(key in result)) {
395+
result[key] = value
396+
}
397+
})
398+
obj = Object.getPrototypeOf(obj)
399+
}
400+
return result
401+
}
402+
329403
count():number {
330404
if (!this.index) {
331405
this.createIndex()

src/lib/adapters/Caching.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { BulkImportResource } from '../interfaces/Resource'
1616

1717
export default class CachingAdapter implements Adapter, BulkImportResource<TItemLocation> {
1818
protected highestId: number
19-
protected bookmarksCache: Folder<TItemLocation>
19+
public bookmarksCache: Folder<TItemLocation>
2020
protected server: any
2121
protected location: TItemLocation = ItemLocation.SERVER
2222

@@ -35,7 +35,7 @@ export default class CachingAdapter implements Adapter, BulkImportResource<TItem
3535
}
3636

3737
async getBookmarksTree(): Promise<Folder<TItemLocation>> {
38-
return this.bookmarksCache.clone()
38+
return this.bookmarksCache.copy()
3939
}
4040

4141
acceptsBookmark(bm:Bookmark<TItemLocation>):boolean {
@@ -53,6 +53,7 @@ export default class CachingAdapter implements Adapter, BulkImportResource<TItem
5353

5454
async createBookmark(bm:Bookmark<TItemLocation>):Promise<string|number> {
5555
Logger.log('CREATE', bm)
56+
bm = bm.copy()
5657
bm.id = ++this.highestId
5758
const foundFolder = this.bookmarksCache.findFolder(bm.parentId)
5859
if (!foundFolder) {
@@ -207,7 +208,7 @@ export default class CachingAdapter implements Adapter, BulkImportResource<TItem
207208
throw new UnknownCreateTargetError()
208209
}
209210
// clone and adjust ids
210-
const imported = folder.clone()
211+
const imported = folder.copy()
211212
imported.id = id
212213
await imported.traverse(async(item, parentFolder) => {
213214
item.id = ++this.highestId

src/lib/adapters/Git.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export default class GitAdapter extends CachingAdapter {
184184
Logger.log('onSyncComplete')
185185
clearInterval(this.lockingInterval)
186186

187-
this.bookmarksCache = this.bookmarksCache.clone()
187+
this.bookmarksCache = this.bookmarksCache.clone(false)
188188
const newTreeHash = await this.bookmarksCache.hash(true)
189189
if (newTreeHash !== this.initialTreeHash) {
190190
const fileContents = this.server.bookmark_file_type === 'xbel' ? createXBEL(this.bookmarksCache, this.highestId) : createHTML(this.bookmarksCache, this.highestId)

0 commit comments

Comments
 (0)