fix(sync): make datalayer upsert idempotent (#2528)

That fix does not really fix the original issue, but it makes it
impactless, and I think it's safer anyway to have upsert idempotent.

The pattern to reproduce is:
- peer A create a synced map, add a datalayer, save it
- peer B loads the map, click on edit
- at this time, peer B have twice the datalayer data, once from the
server AND once from the sync

So a better fix would be to make that peer B send a meaningfull HLC to
peer A I guess.
For this we may save the last HLC is the map properties, or maybe try to
merge the "reference_version" and the HLC.
This commit is contained in:
Yohan Boniface 2025-02-27 15:32:09 +01:00 committed by GitHub
commit e919c5f168
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 33 additions and 1 deletions

View file

@ -54,7 +54,11 @@ export class MapUpdater extends BaseUpdater {
export class DataLayerUpdater extends BaseUpdater { export class DataLayerUpdater extends BaseUpdater {
upsert({ value }) { upsert({ value }) {
// Upsert only happens when a new datalayer is created. // Upsert only happens when a new datalayer is created.
const datalayer = this._umap.createDataLayer(value, false) try {
this.getDataLayerFromID(value.id)
} catch {
this._umap.createDataLayer(value, false)
}
} }
update({ key, metadata, value }) { update({ key, metadata, value }) {

View file

@ -568,6 +568,34 @@ def test_create_and_sync_map(new_page, asgi_live_server, tilelayer, login, user)
expect(markersB).to_have_count(2) expect(markersB).to_have_count(2)
@pytest.mark.xdist_group(name="websockets")
def test_saved_datalayer_are_not_duplicated(new_page, asgi_live_server, tilelayer):
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
map.settings["properties"]["syncEnabled"] = True
map.save()
# Create one tab
peerA = new_page("Page A")
peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
# Create a new datalayer
peerA.get_by_title("Manage layers").click()
peerA.get_by_title("Add a layer").click()
peerA.locator("#map").click(position={"x": 220, "y": 220})
# Save layer to the server, so now the datalayer exist on the server AND
# is still in the live operations of peer A
with peerA.expect_response(re.compile(".*/datalayer/create/.*")):
peerA.get_by_role("button", name="Save").click()
# Now load the map from another tab
peerB = new_page("Page B")
peerB.goto(peerA.url)
peerB.get_by_role("button", name="Open browser").click()
expect(peerB.get_by_text("Layer 1")).to_be_visible()
peerB.get_by_role("button", name="Edit").click()
peerA.wait_for_timeout(300) # Let the synchro roll on.
expect(peerB.get_by_text("Layer 1")).to_be_visible()
@pytest.mark.xdist_group(name="websockets") @pytest.mark.xdist_group(name="websockets")
def test_should_sync_saved_status(new_page, asgi_live_server, tilelayer): def test_should_sync_saved_status(new_page, asgi_live_server, tilelayer):
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS) map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)