Skip to content

Commit d4fb92e

Browse files
Fix duplicated content during sync (#2388)
Here are the main fixes: - mark a synched datalayer as loaded (so the peer does not try to get data from the server) - do not mark synched datalayers as dirty - properly consume the lastKnownHLC, so to get an accurate list of operations fix #2219
2 parents 650110f + 36fdb81 commit d4fb92e

File tree

6 files changed

+95
-15
lines changed

6 files changed

+95
-15
lines changed

umap/static/umap/js/modules/data/layer.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ export class DataLayer extends ServerStored {
8888

8989
if (!this.createdOnServer) {
9090
if (this.showAtLoad()) this.show()
91-
this.isDirty = true
9291
}
9392

9493
// Only layers that are displayed on load must be hidden/shown
@@ -151,7 +150,6 @@ export class DataLayer extends ServerStored {
151150
for (const field of fields) {
152151
this.layer.onEdit(field, builder)
153152
}
154-
this.redraw()
155153
this.show()
156154
break
157155
case 'remote-data':
@@ -592,7 +590,7 @@ export class DataLayer extends ServerStored {
592590
options.name = translate('Clone of {name}', { name: this.options.name })
593591
delete options.id
594592
const geojson = Utils.CopyJSON(this._geojson)
595-
const datalayer = this._umap.createDataLayer(options)
593+
const datalayer = this._umap.createDirtyDataLayer(options)
596594
datalayer.fromGeoJSON(geojson)
597595
return datalayer
598596
}
@@ -1066,7 +1064,7 @@ export class DataLayer extends ServerStored {
10661064

10671065
setReferenceVersion({ response, sync }) {
10681066
this._referenceVersion = response.headers.get('X-Datalayer-Version')
1069-
this.sync.update('_referenceVersion', this._referenceVersion)
1067+
if (sync) this.sync.update('_referenceVersion', this._referenceVersion)
10701068
}
10711069

10721070
async save() {

umap/static/umap/js/modules/importer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export default class Importer extends Utils.WithTemplate {
161161
get layer() {
162162
return (
163163
this._umap.datalayers[this.layerId] ||
164-
this._umap.createDataLayer({ name: this.layerName })
164+
this._umap.createDirtyDataLayer({ name: this.layerName })
165165
)
166166
}
167167

umap/static/umap/js/modules/sync/engine.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,13 @@ export class SyncEngine {
222222
* @param {string} payload.sender the uuid of the requesting peer
223223
* @param {string} payload.latestKnownHLC the latest known HLC of the requesting peer
224224
*/
225-
onListOperationsRequest({ sender, lastKnownHLC }) {
225+
onListOperationsRequest({ sender, message }) {
226+
debug(
227+
`received operations request from peer ${sender} (since ${message.lastKnownHLC})`
228+
)
229+
226230
this.sendToPeer(sender, 'ListOperationsResponse', {
227-
operations: this._operations.getOperationsSince(lastKnownHLC),
231+
operations: this._operations.getOperationsSince(message.lastKnownHLC),
228232
})
229233
}
230234

@@ -485,5 +489,5 @@ export class Operations {
485489
}
486490

487491
function debug(...args) {
488-
console.debug('SYNC ⇆', ...args)
492+
console.debug('SYNC ⇆', ...args.map((x) => JSON.stringify(x)))
489493
}

umap/static/umap/js/modules/sync/updaters.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ export class MapUpdater extends BaseUpdater {
5454
export class DataLayerUpdater extends BaseUpdater {
5555
upsert({ value }) {
5656
// Upsert only happens when a new datalayer is created.
57-
this._umap.createDataLayer(value, false)
57+
const datalayer = this._umap.createDataLayer(value, false)
58+
// Prevent the layer to get data from the server, as it will get it
59+
// from the sync.
60+
datalayer._loaded = true
5861
}
5962

6063
update({ key, metadata, value }) {

umap/static/umap/js/modules/umap.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -324,14 +324,14 @@ export default class Umap extends ServerStored {
324324
dataUrl = decodeURIComponent(dataUrl)
325325
dataUrl = this.renderUrl(dataUrl)
326326
dataUrl = this.proxyUrl(dataUrl)
327-
const datalayer = this.createDataLayer()
327+
const datalayer = this.createDirtyDataLayer()
328328
await datalayer
329329
.importFromUrl(dataUrl, dataFormat)
330330
.then(() => datalayer.zoomTo())
331331
}
332332
} else if (data) {
333333
data = decodeURIComponent(data)
334-
const datalayer = this.createDataLayer()
334+
const datalayer = this.createDirtyDataLayer()
335335
await datalayer.importRaw(data, dataFormat).then(() => datalayer.zoomTo())
336336
}
337337
}
@@ -599,8 +599,14 @@ export default class Umap extends ServerStored {
599599
return datalayer
600600
}
601601

602+
createDirtyDataLayer(options) {
603+
const datalayer = this.createDataLayer(options, true)
604+
datalayer.isDirty = true
605+
return datalayer
606+
}
607+
602608
newDataLayer() {
603-
const datalayer = this.createDataLayer({})
609+
const datalayer = this.createDirtyDataLayer({})
604610
datalayer.edit()
605611
}
606612

@@ -1389,7 +1395,7 @@ export default class Umap extends ServerStored {
13891395
fallback.show()
13901396
return fallback
13911397
}
1392-
return this.createDataLayer()
1398+
return this.createDirtyDataLayer()
13931399
}
13941400

13951401
findDataLayer(method, context) {
@@ -1553,7 +1559,7 @@ export default class Umap extends ServerStored {
15531559
if (type === 'umap') {
15541560
this.importUmapFile(file, 'umap')
15551561
} else {
1556-
if (!layer) layer = this.createDataLayer({ name: file.name })
1562+
if (!layer) layer = this.createDirtyDataLayer({ name: file.name })
15571563
layer.importFromFile(file, type)
15581564
}
15591565
}
@@ -1579,7 +1585,7 @@ export default class Umap extends ServerStored {
15791585
delete geojson._storage
15801586
}
15811587
delete geojson._umap_options?.id // Never trust an id at this stage
1582-
const dataLayer = this.createDataLayer(geojson._umap_options)
1588+
const dataLayer = this.createDirtyDataLayer(geojson._umap_options)
15831589
dataLayer.fromUmapGeoJSON(geojson)
15841590
}
15851591

umap/tests/integration/test_websocket_sync.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ def test_websocket_connection_can_sync_markers(
3939
a_map_el.click(position={"x": 220, "y": 220})
4040
expect(a_marker_pane).to_have_count(1)
4141
expect(b_marker_pane).to_have_count(1)
42+
# Peer B should not be in state dirty
43+
expect(peerB.get_by_role("button", name="View")).to_be_visible()
44+
expect(peerB.get_by_role("button", name="Cancel edits")).to_be_hidden()
4245
peerA.locator("body").type("Synced name")
4346
peerA.locator("body").press("Escape")
4447

@@ -415,3 +418,69 @@ def test_should_sync_datalayers(new_page, live_server, websocket_server, tilelay
415418
peerA.get_by_role("button", name="Save").click()
416419

417420
assert DataLayer.objects.count() == 2
421+
422+
423+
@pytest.mark.xdist_group(name="websockets")
424+
def test_create_and_sync_map(
425+
new_page, live_server, websocket_server, tilelayer, login, user
426+
):
427+
# Create a syncable map with peerA
428+
peerA = login(user, prefix="Page A")
429+
peerA.goto(f"{live_server.url}/en/map/new/")
430+
with peerA.expect_response(re.compile("./map/create/.*")):
431+
peerA.get_by_role("button", name="Save Draft").click()
432+
peerA.get_by_role("link", name="Map advanced properties").click()
433+
peerA.get_by_text("Real-time collaboration", exact=True).click()
434+
peerA.get_by_text("Enable real-time").click()
435+
peerA.get_by_role("link", name="Update permissions and editors").click()
436+
peerA.locator('select[name="share_status"]').select_option(str(Map.PUBLIC))
437+
with peerA.expect_response(re.compile("./update/settings/.*")):
438+
peerA.get_by_role("button", name="Save").click()
439+
expect(peerA.get_by_role("button", name="Cancel edits")).to_be_hidden()
440+
# Quit edit mode
441+
peerA.get_by_role("button", name="View").click()
442+
443+
# Open map and go to edit mode with peer B
444+
peerB = new_page("Page B")
445+
peerB.goto(peerA.url)
446+
peerB.get_by_role("button", name="Edit").click()
447+
448+
# Create a marker from peerA
449+
markersA = peerA.locator(".leaflet-marker-pane > div")
450+
markersB = peerB.locator(".leaflet-marker-pane > div")
451+
expect(markersA).to_have_count(0)
452+
expect(markersB).to_have_count(0)
453+
454+
# Add a marker from peer A
455+
peerA.get_by_role("button", name="Edit").click()
456+
peerA.get_by_title("Draw a marker").click()
457+
peerA.locator("#map").click(position={"x": 220, "y": 220})
458+
expect(markersA).to_have_count(1)
459+
expect(markersB).to_have_count(1)
460+
461+
# Save and quit edit mode again
462+
with peerA.expect_response(re.compile("./datalayer/create/.*")):
463+
peerA.get_by_role("button", name="Save").click()
464+
peerA.get_by_role("button", name="View").click()
465+
expect(markersA).to_have_count(1)
466+
expect(markersB).to_have_count(1)
467+
peerA.wait_for_timeout(500)
468+
expect(markersA).to_have_count(1)
469+
expect(markersB).to_have_count(1)
470+
471+
# Peer B should not be in state dirty
472+
expect(peerB.get_by_role("button", name="View")).to_be_visible()
473+
expect(peerB.get_by_role("button", name="Cancel edits")).to_be_hidden()
474+
475+
# Add a marker from peer B
476+
peerB.get_by_title("Draw a marker").click()
477+
peerB.locator("#map").click(position={"x": 200, "y": 200})
478+
expect(markersB).to_have_count(2)
479+
expect(markersA).to_have_count(1)
480+
with peerB.expect_response(re.compile("./datalayer/update/.*")):
481+
peerB.get_by_role("button", name="Save").click()
482+
expect(markersB).to_have_count(2)
483+
expect(markersA).to_have_count(1)
484+
peerA.get_by_role("button", name="Edit").click()
485+
expect(markersA).to_have_count(2)
486+
expect(markersB).to_have_count(2)

0 commit comments

Comments
 (0)