Compare commits

..

No commits in common. "d4fb92ec56a410b94e9ee352cfb0b4c81b6a93db" and "24511d796dd020669b042d907b38b9f5989464c5" have entirely different histories.

13 changed files with 124 additions and 304 deletions

View file

@ -32,10 +32,6 @@
background-color: var(--color-lightCyan); background-color: var(--color-lightCyan);
color: var(--color-dark); color: var(--color-dark);
} }
.dark .off.connected-peers {
background-color: var(--color-lightGray);
color: var(--color-darkGray);
}
.leaflet-container .edit-cancel, .leaflet-container .edit-cancel,
.leaflet-container .edit-disable, .leaflet-container .edit-disable,

View file

@ -1,39 +1,11 @@
import { DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from './i18n.js' import { translate } from './i18n.js'
import * as Utils from './utils.js' import * as Utils from './utils.js'
const TEMPLATE = ` export default class Caption {
<div class="umap-caption">
<hgroup>
<h3>
<i class="icon icon-16 icon-caption icon-block"></i>
<span class="map-name" data-ref="name"></span>
</h3>
<h4 data-ref="author"></h4>
</hgroup>
<div class="umap-map-description text" data-ref="description"></div>
<div class="datalayer-container" data-ref="datalayersContainer"></div>
<div class="credits-container">
<details>
<summary>${translate('Credits')}</summary>
<fieldset>
<h5>${translate('User content credits')}</h5>
<p data-ref="userCredits"></p>
<p data-ref="licence">${translate('Map user content has been published under licence')} <a href="#" data-ref="licenceLink"></a></p>
<p data-ref="noLicence">${translate('No licence has been set')}</p>
<h5>${translate('Map background credits')}</h5>
<p><strong data-ref="bgName"></strong> <span data-ref="bgAttribution"></span></p>
<p data-ref="poweredBy"></p>
</fieldset>
</details>
</div>
</div>`
export default class Caption extends Utils.WithTemplate {
constructor(umap, leafletMap) { constructor(umap, leafletMap) {
super()
this._umap = umap this._umap = umap
this._leafletMap = leafletMap this._leafletMap = leafletMap
this.loadTemplate(TEMPLATE)
} }
isOpen() { isOpen() {
@ -46,67 +18,94 @@ export default class Caption extends Utils.WithTemplate {
} }
open() { open() {
this.elements.name.textContent = this._umap.getDisplayName() const container = DomUtil.create('div', 'umap-caption')
this.elements.author.innerHTML = '' const hgroup = DomUtil.element({ tagName: 'hgroup', parent: container })
this._umap.addAuthorLink(this.elements.author) DomUtil.createTitle(
if (this._umap.properties.description) { hgroup,
this.elements.description.innerHTML = Utils.toHTML( this._umap.getDisplayName(),
this._umap.properties.description 'icon-caption icon-block',
) 'map-name'
} else {
this.elements.description.hidden = true
}
this.elements.datalayersContainer.innerHTML = ''
this._umap.eachDataLayerReverse((datalayer) =>
this.addDataLayer(datalayer, this.elements.datalayersContainer)
) )
this.addCredits() const title = Utils.loadTemplate('<h4></h4>')
this._umap.panel.open({ content: this.element }).then(() => { hgroup.appendChild(title)
this._umap.addAuthorLink(title)
if (this._umap.properties.description) {
const description = DomUtil.element({
tagName: 'div',
className: 'umap-map-description text',
safeHTML: Utils.toHTML(this._umap.properties.description),
parent: container,
})
}
const datalayerContainer = DomUtil.create('div', 'datalayer-container', container)
this._umap.eachDataLayerReverse((datalayer) =>
this.addDataLayer(datalayer, datalayerContainer)
)
const creditsContainer = DomUtil.create('div', 'credits-container', container)
this.addCredits(creditsContainer)
this._umap.panel.open({ content: container }).then(() => {
// Create the legend when the panel is actually on the DOM // Create the legend when the panel is actually on the DOM
this._umap.eachDataLayerReverse((datalayer) => datalayer.renderLegend()) this._umap.eachDataLayerReverse((datalayer) => datalayer.renderLegend())
}) })
} }
addDataLayer(datalayer, parent) { addDataLayer(datalayer, container) {
if (!datalayer.options.inCaption) return if (!datalayer.options.inCaption) return
const template = ` const p = DomUtil.create('p', `caption-item ${datalayer.cssId}`, container)
<p class="caption-item ${datalayer.cssId}"> const legend = DomUtil.create('span', 'datalayer-legend', p)
<span class="datalayer-legend"></span> const headline = DomUtil.create('strong', '', p)
<strong data-ref="toolbox"></strong>
<span class="text" data-ref="description"></span>
</p>`
const [element, { toolbox, description }] = Utils.loadTemplateWithRefs(template)
if (datalayer.options.description) { if (datalayer.options.description) {
description.innerHTML = Utils.toHTML(datalayer.options.description) DomUtil.element({
} else { tagName: 'span',
description.hidden = true parent: p,
safeHTML: Utils.toHTML(datalayer.options.description),
})
} }
datalayer.renderToolbox(toolbox) datalayer.renderToolbox(headline)
parent.appendChild(element) DomUtil.add('span', '', headline, `${datalayer.options.name} `)
// Use textContent for security
const name = Utils.loadTemplate('<span></span>')
name.textContent = datalayer.options.name
toolbox.appendChild(name)
} }
addCredits() { addCredits(container) {
const credits = DomUtil.createFieldset(container, translate('Credits'))
let title = DomUtil.add('h5', '', credits, translate('User content credits'))
if (this._umap.properties.shortCredit || this._umap.properties.longCredit) { if (this._umap.properties.shortCredit || this._umap.properties.longCredit) {
this.elements.userCredits.innerHTML = Utils.toHTML( DomUtil.element({
this._umap.properties.longCredit || this._umap.properties.shortCredit tagName: 'p',
) parent: credits,
} else { safeHTML: Utils.toHTML(
this.elements.userCredits.hidden = true this._umap.properties.longCredit || this._umap.properties.shortCredit
),
})
} }
if (this._umap.properties.licence) { if (this._umap.properties.licence) {
this.elements.licenceLink.href = this._umap.properties.licence.url const licence = DomUtil.add(
this.elements.licenceLink.textContent = this._umap.properties.licence.name 'p',
this.elements.noLicence.hidden = true '',
credits,
`${translate('Map user content has been published under licence')} `
)
DomUtil.createLink(
'',
licence,
this._umap.properties.licence.name,
this._umap.properties.licence.url
)
} else { } else {
this.elements.licence.hidden = true DomUtil.add('p', '', credits, translate('No licence has been set'))
} }
this.elements.bgName.textContent = this._leafletMap.selectedTilelayer.options.name title = DomUtil.create('h5', '', credits)
this.elements.bgAttribution.innerHTML = title.textContent = translate('Map background credits')
this._leafletMap.selectedTilelayer.getAttribution() const tilelayerCredit = DomUtil.create('p', '', credits)
DomUtil.element({
tagName: 'strong',
parent: tilelayerCredit,
textContent: `${this._leafletMap.selectedTilelayer.options.name} `,
})
DomUtil.element({
tagName: 'span',
parent: tilelayerCredit,
safeHTML: this._leafletMap.selectedTilelayer.getAttribution(),
})
const urls = { const urls = {
leaflet: 'http://leafletjs.com', leaflet: 'http://leafletjs.com',
django: 'https://www.djangoproject.com', django: 'https://www.djangoproject.com',
@ -114,7 +113,7 @@ export default class Caption extends Utils.WithTemplate {
changelog: 'https://docs.umap-project.org/en/master/changelog/', changelog: 'https://docs.umap-project.org/en/master/changelog/',
version: this._umap.properties.umap_version, version: this._umap.properties.umap_version,
} }
this.elements.poweredBy.innerHTML = translate( const creditHTML = translate(
` `
Powered by <a href="{leaflet}">Leaflet</a> and Powered by <a href="{leaflet}">Leaflet</a> and
<a href="{django}">Django</a>, <a href="{django}">Django</a>,
@ -123,5 +122,6 @@ export default class Caption extends Utils.WithTemplate {
`, `,
urls urls
) )
DomUtil.element({ tagName: 'p', innerHTML: creditHTML, parent: credits })
} }
} }

View file

@ -20,7 +20,7 @@ import loadPopup from '../rendering/popup.js'
class Feature { class Feature {
constructor(umap, datalayer, geojson = {}, id = null) { constructor(umap, datalayer, geojson = {}, id = null) {
this._umap = umap this._umap = umap
this.sync = umap.syncEngine.proxy(this) this.sync = umap.sync_engine.proxy(this)
this._marked_for_deletion = false this._marked_for_deletion = false
this._isDirty = false this._isDirty = false
this._ui = null this._ui = null

View file

@ -41,7 +41,7 @@ export class DataLayer extends ServerStored {
constructor(umap, leafletMap, data = {}) { constructor(umap, leafletMap, data = {}) {
super() super()
this._umap = umap this._umap = umap
this.sync = umap.syncEngine.proxy(this) this.sync = umap.sync_engine.proxy(this)
this._index = Array() this._index = Array()
this._features = {} this._features = {}
this._geojson = null this._geojson = null
@ -88,6 +88,7 @@ export class DataLayer extends ServerStored {
if (!this.createdOnServer) { if (!this.createdOnServer) {
if (this.showAtLoad()) this.show() if (this.showAtLoad()) this.show()
this.isDirty = true
} }
// Only layers that are displayed on load must be hidden/shown // Only layers that are displayed on load must be hidden/shown
@ -150,6 +151,7 @@ export class DataLayer extends ServerStored {
for (const field of fields) { for (const field of fields) {
this.layer.onEdit(field, builder) this.layer.onEdit(field, builder)
} }
this.redraw()
this.show() this.show()
break break
case 'remote-data': case 'remote-data':
@ -590,7 +592,7 @@ export class DataLayer extends ServerStored {
options.name = translate('Clone of {name}', { name: this.options.name }) options.name = translate('Clone of {name}', { name: this.options.name })
delete options.id delete options.id
const geojson = Utils.CopyJSON(this._geojson) const geojson = Utils.CopyJSON(this._geojson)
const datalayer = this._umap.createDirtyDataLayer(options) const datalayer = this._umap.createDataLayer(options)
datalayer.fromGeoJSON(geojson) datalayer.fromGeoJSON(geojson)
return datalayer return datalayer
} }
@ -1064,7 +1066,7 @@ export class DataLayer extends ServerStored {
setReferenceVersion({ response, sync }) { setReferenceVersion({ response, sync }) {
this._referenceVersion = response.headers.get('X-Datalayer-Version') this._referenceVersion = response.headers.get('X-Datalayer-Version')
if (sync) this.sync.update('_referenceVersion', this._referenceVersion) this.sync.update('_referenceVersion', this._referenceVersion)
} }
async save() { async save() {

View file

@ -161,7 +161,7 @@ export default class Importer extends Utils.WithTemplate {
get layer() { get layer() {
return ( return (
this._umap.datalayers[this.layerId] || this._umap.datalayers[this.layerId] ||
this._umap.createDirtyDataLayer({ name: this.layerName }) this._umap.createDataLayer({ name: this.layerName })
) )
} }

View file

@ -3,12 +3,6 @@ import { HybridLogicalClock } from './hlc.js'
import { DataLayerUpdater, FeatureUpdater, MapUpdater } from './updaters.js' import { DataLayerUpdater, FeatureUpdater, MapUpdater } from './updaters.js'
import { WebSocketTransport } from './websocket.js' import { WebSocketTransport } from './websocket.js'
// Start reconnecting after 2 seconds, then double the delay each time
// maxing out at 32 seconds.
const RECONNECT_DELAY = 2000
const RECONNECT_DELAY_FACTOR = 2
const MAX_RECONNECT_DELAY = 32000
/** /**
* The syncEngine exposes an API to sync messages between peers over the network. * The syncEngine exposes an API to sync messages between peers over the network.
* *
@ -48,65 +42,32 @@ const MAX_RECONNECT_DELAY = 32000
* ``` * ```
*/ */
export class SyncEngine { export class SyncEngine {
constructor(umap) { constructor(map) {
this._umap = umap
this.updaters = { this.updaters = {
map: new MapUpdater(umap), map: new MapUpdater(map),
feature: new FeatureUpdater(umap), feature: new FeatureUpdater(map),
datalayer: new DataLayerUpdater(umap), datalayer: new DataLayerUpdater(map),
} }
this.transport = undefined this.transport = undefined
this._operations = new Operations() this._operations = new Operations()
this._reconnectTimeout = null
this._reconnectDelay = RECONNECT_DELAY
this.websocketConnected = false
} }
async authenticate() { async authenticate(tokenURI, webSocketURI, server) {
const websocketTokenURI = this._umap.urls.get('map_websocket_auth_token', { const [response, _, error] = await server.get(tokenURI)
map_id: this._umap.id,
})
const [response, _, error] = await this._umap.server.get(websocketTokenURI)
if (!error) { if (!error) {
this.start(response.token) this.start(webSocketURI, response.token)
} }
} }
start(authToken) { start(webSocketURI, authToken) {
this.transport = new WebSocketTransport( this.transport = new WebSocketTransport(webSocketURI, authToken, this)
this._umap.properties.websocketURI,
authToken,
this
)
} }
stop() { stop() {
if (this.transport) { if (this.transport) this.transport.close()
this.transport.close()
}
this.transport = undefined this.transport = undefined
} }
onConnection() {
this._reconnectTimeout = null
this._reconnectDelay = RECONNECT_DELAY
this.websocketConnected = true
this.updaters.map.update({ key: 'numberOfConnectedPeers' })
}
reconnect() {
this.websocketConnected = false
this.updaters.map.update({ key: 'numberOfConnectedPeers' })
this._reconnectTimeout = setTimeout(() => {
if (this._reconnectDelay < MAX_RECONNECT_DELAY) {
this._reconnectDelay = this._reconnectDelay * RECONNECT_DELAY_FACTOR
}
this.authenticate()
}, this._reconnectDelay)
}
upsert(subject, metadata, value) { upsert(subject, metadata, value) {
this._send({ verb: 'upsert', subject, metadata, value }) this._send({ verb: 'upsert', subject, metadata, value })
} }
@ -222,13 +183,9 @@ export class SyncEngine {
* @param {string} payload.sender the uuid of the requesting peer * @param {string} payload.sender the uuid of the requesting peer
* @param {string} payload.latestKnownHLC the latest known HLC of the requesting peer * @param {string} payload.latestKnownHLC the latest known HLC of the requesting peer
*/ */
onListOperationsRequest({ sender, message }) { onListOperationsRequest({ sender, lastKnownHLC }) {
debug(
`received operations request from peer ${sender} (since ${message.lastKnownHLC})`
)
this.sendToPeer(sender, 'ListOperationsResponse', { this.sendToPeer(sender, 'ListOperationsResponse', {
operations: this._operations.getOperationsSince(message.lastKnownHLC), operations: this._operations.getOperationsSince(lastKnownHLC),
}) })
} }
@ -489,5 +446,5 @@ export class Operations {
} }
function debug(...args) { function debug(...args) {
console.debug('SYNC ⇆', ...args.map((x) => JSON.stringify(x))) console.debug('SYNC ⇆', ...args)
} }

View file

@ -54,10 +54,7 @@ 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) this._umap.createDataLayer(value, false)
// Prevent the layer to get data from the server, as it will get it
// from the sync.
datalayer._loaded = true
} }
update({ key, metadata, value }) { update({ key, metadata, value }) {

View file

@ -1,59 +1,15 @@
const PONG_TIMEOUT = 5000
const PING_INTERVAL = 30000
const FIRST_CONNECTION_TIMEOUT = 2000
export class WebSocketTransport { export class WebSocketTransport {
constructor(webSocketURI, authToken, messagesReceiver) { constructor(webSocketURI, authToken, messagesReceiver) {
this.receiver = messagesReceiver
this.closeRequested = false
this.websocket = new WebSocket(webSocketURI) this.websocket = new WebSocket(webSocketURI)
this.websocket.onopen = () => { this.websocket.onopen = () => {
this.send('JoinRequest', { token: authToken }) this.send('JoinRequest', { token: authToken })
this.receiver.onConnection()
} }
this.websocket.addEventListener('message', this.onMessage.bind(this)) this.websocket.addEventListener('message', this.onMessage.bind(this))
this.websocket.onclose = () => { this.receiver = messagesReceiver
console.log('websocket closed')
if (!this.closeRequested) {
console.log('Not requested, reconnecting...')
this.receiver.reconnect()
}
}
this.ensureOpen = setInterval(() => {
if (this.websocket.readyState !== WebSocket.OPEN) {
this.websocket.close()
clearInterval(this.ensureOpen)
}
}, FIRST_CONNECTION_TIMEOUT)
// To ensure the connection is still alive, we send ping and expect pong back.
// Websocket provides a `ping` method to keep the connection alive, but it's
// unfortunately not possible to access it from the WebSocket object.
// See https://making.close.com/posts/reliable-websockets/ for more details.
this.pingInterval = setInterval(() => {
if (this.websocket.readyState === WebSocket.OPEN) {
this.websocket.send('ping')
this.pongReceived = false
setTimeout(() => {
if (!this.pongReceived) {
console.warn('No pong received, reconnecting...')
this.websocket.close()
clearInterval(this.pingInterval)
}
}, PONG_TIMEOUT)
}
}, PING_INTERVAL)
} }
onMessage(wsMessage) { onMessage(wsMessage) {
if (wsMessage.data === 'pong') { this.receiver.receive(JSON.parse(wsMessage.data))
this.pongReceived = true
} else {
this.receiver.receive(JSON.parse(wsMessage.data))
}
} }
send(kind, payload) { send(kind, payload) {
@ -64,7 +20,6 @@ export class WebSocketTransport {
} }
close() { close() {
this.closeRequested = true
this.websocket.close() this.websocket.close()
} }
} }

View file

@ -61,6 +61,8 @@ export default class Umap extends ServerStored {
) )
this.searchParams = new URLSearchParams(window.location.search) this.searchParams = new URLSearchParams(window.location.search)
this.sync_engine = new SyncEngine(this)
this.sync = this.sync_engine.proxy(this)
// Locale name (pt_PT, en_US…) // Locale name (pt_PT, en_US…)
// To be used for Django localization // To be used for Django localization
if (geojson.properties.locale) setLocale(geojson.properties.locale) if (geojson.properties.locale) setLocale(geojson.properties.locale)
@ -122,9 +124,6 @@ export default class Umap extends ServerStored {
this.share = new Share(this) this.share = new Share(this)
this.rules = new Rules(this) this.rules = new Rules(this)
this.syncEngine = new SyncEngine(this)
this.sync = this.syncEngine.proxy(this)
if (this.hasEditMode()) { if (this.hasEditMode()) {
this.editPanel = new EditPanel(this, this._leafletMap) this.editPanel = new EditPanel(this, this._leafletMap)
this.fullPanel = new FullPanel(this, this._leafletMap) this.fullPanel = new FullPanel(this, this._leafletMap)
@ -324,14 +323,14 @@ export default class Umap extends ServerStored {
dataUrl = decodeURIComponent(dataUrl) dataUrl = decodeURIComponent(dataUrl)
dataUrl = this.renderUrl(dataUrl) dataUrl = this.renderUrl(dataUrl)
dataUrl = this.proxyUrl(dataUrl) dataUrl = this.proxyUrl(dataUrl)
const datalayer = this.createDirtyDataLayer() const datalayer = this.createDataLayer()
await datalayer await datalayer
.importFromUrl(dataUrl, dataFormat) .importFromUrl(dataUrl, dataFormat)
.then(() => datalayer.zoomTo()) .then(() => datalayer.zoomTo())
} }
} else if (data) { } else if (data) {
data = decodeURIComponent(data) data = decodeURIComponent(data)
const datalayer = this.createDirtyDataLayer() const datalayer = this.createDataLayer()
await datalayer.importRaw(data, dataFormat).then(() => datalayer.zoomTo()) await datalayer.importRaw(data, dataFormat).then(() => datalayer.zoomTo())
} }
} }
@ -599,14 +598,8 @@ export default class Umap extends ServerStored {
return datalayer return datalayer
} }
createDirtyDataLayer(options) {
const datalayer = this.createDataLayer(options, true)
datalayer.isDirty = true
return datalayer
}
newDataLayer() { newDataLayer() {
const datalayer = this.createDirtyDataLayer({}) const datalayer = this.createDataLayer({})
datalayer.edit() datalayer.edit()
} }
@ -1264,13 +1257,18 @@ export default class Umap extends ServerStored {
} }
async initSyncEngine() { async initSyncEngine() {
// this.properties.websocketEnabled is set by the server admin
if (this.properties.websocketEnabled === false) return if (this.properties.websocketEnabled === false) return
// this.properties.syncEnabled is set by the user in the map settings
if (this.properties.syncEnabled !== true) { if (this.properties.syncEnabled !== true) {
this.sync.stop() this.sync.stop()
} else { } else {
await this.sync.authenticate() const ws_token_uri = this.urls.get('map_websocket_auth_token', {
map_id: this.id,
})
await this.sync.authenticate(
ws_token_uri,
this.properties.websocketURI,
this.server
)
} }
} }
@ -1345,17 +1343,7 @@ export default class Umap extends ServerStored {
}, },
numberOfConnectedPeers: () => { numberOfConnectedPeers: () => {
Utils.eachElement('.connected-peers span', (el) => { Utils.eachElement('.connected-peers span', (el) => {
if (this.sync.websocketConnected) { el.textContent = this.sync.getNumberOfConnectedPeers()
el.textContent = this.sync.getNumberOfConnectedPeers()
} else {
el.textContent = translate('Disconnected')
}
el.parentElement.classList.toggle('off', !this.sync.websocketConnected)
})
},
'properties.starred': () => {
Utils.eachElement('.map-star', (el) => {
el.classList.toggle('starred', this.properties.starred)
}) })
}, },
} }
@ -1395,7 +1383,7 @@ export default class Umap extends ServerStored {
fallback.show() fallback.show()
return fallback return fallback
} }
return this.createDirtyDataLayer() return this.createDataLayer()
} }
findDataLayer(method, context) { findDataLayer(method, context) {
@ -1543,7 +1531,7 @@ export default class Umap extends ServerStored {
? translate('Map has been starred') ? translate('Map has been starred')
: translate('Map has been unstarred') : translate('Map has been unstarred')
) )
this.render(['properties.starred']) this.render(['starred'])
} }
processFileToImport(file, layer, type) { processFileToImport(file, layer, type) {
@ -1559,7 +1547,7 @@ export default class Umap extends ServerStored {
if (type === 'umap') { if (type === 'umap') {
this.importUmapFile(file, 'umap') this.importUmapFile(file, 'umap')
} else { } else {
if (!layer) layer = this.createDirtyDataLayer({ name: file.name }) if (!layer) layer = this.createDataLayer({ name: file.name })
layer.importFromFile(file, type) layer.importFromFile(file, type)
} }
} }
@ -1585,7 +1573,7 @@ export default class Umap extends ServerStored {
delete geojson._storage delete geojson._storage
} }
delete geojson._umap_options?.id // Never trust an id at this stage delete geojson._umap_options?.id // Never trust an id at this stage
const dataLayer = this.createDirtyDataLayer(geojson._umap_options) const dataLayer = this.createDataLayer(geojson._umap_options)
dataLayer.fromUmapGeoJSON(geojson) dataLayer.fromUmapGeoJSON(geojson)
} }

View file

@ -541,7 +541,11 @@ U.StarControl = L.Control.Button.extend({
options: { options: {
position: 'topleft', position: 'topleft',
title: L._('Star this map'), title: L._('Star this map'),
className: 'leaflet-control-star map-star umap-control', },
getClassName: function () {
const status = this._umap.properties.starred ? ' starred' : ''
return `leaflet-control-star umap-control${status}`
}, },
onClick: function () { onClick: function () {

View file

@ -8,11 +8,8 @@ import { MapUpdater } from '../js/modules/sync/updaters.js'
import { SyncEngine, Operations } from '../js/modules/sync/engine.js' import { SyncEngine, Operations } from '../js/modules/sync/engine.js'
describe('SyncEngine', () => { describe('SyncEngine', () => {
const websocketTokenURI = 'http://localhost:8000/api/v1/maps/1/websocket_auth_token/'
const websocketURI = 'ws://localhost:8000/ws/maps/1/'
it('should initialize methods even before start', () => { it('should initialize methods even before start', () => {
const engine = new SyncEngine({}, websocketTokenURI, websocketURI) const engine = new SyncEngine({})
engine.upsert() engine.upsert()
engine.update() engine.update()
engine.delete() engine.delete()

View file

@ -39,9 +39,6 @@ def test_websocket_connection_can_sync_markers(
a_map_el.click(position={"x": 220, "y": 220}) a_map_el.click(position={"x": 220, "y": 220})
expect(a_marker_pane).to_have_count(1) expect(a_marker_pane).to_have_count(1)
expect(b_marker_pane).to_have_count(1) expect(b_marker_pane).to_have_count(1)
# Peer B should not be in state dirty
expect(peerB.get_by_role("button", name="View")).to_be_visible()
expect(peerB.get_by_role("button", name="Cancel edits")).to_be_hidden()
peerA.locator("body").type("Synced name") peerA.locator("body").type("Synced name")
peerA.locator("body").press("Escape") peerA.locator("body").press("Escape")
@ -418,69 +415,3 @@ def test_should_sync_datalayers(new_page, live_server, websocket_server, tilelay
peerA.get_by_role("button", name="Save").click() peerA.get_by_role("button", name="Save").click()
assert DataLayer.objects.count() == 2 assert DataLayer.objects.count() == 2
@pytest.mark.xdist_group(name="websockets")
def test_create_and_sync_map(
new_page, live_server, websocket_server, tilelayer, login, user
):
# Create a syncable map with peerA
peerA = login(user, prefix="Page A")
peerA.goto(f"{live_server.url}/en/map/new/")
with peerA.expect_response(re.compile("./map/create/.*")):
peerA.get_by_role("button", name="Save Draft").click()
peerA.get_by_role("link", name="Map advanced properties").click()
peerA.get_by_text("Real-time collaboration", exact=True).click()
peerA.get_by_text("Enable real-time").click()
peerA.get_by_role("link", name="Update permissions and editors").click()
peerA.locator('select[name="share_status"]').select_option(str(Map.PUBLIC))
with peerA.expect_response(re.compile("./update/settings/.*")):
peerA.get_by_role("button", name="Save").click()
expect(peerA.get_by_role("button", name="Cancel edits")).to_be_hidden()
# Quit edit mode
peerA.get_by_role("button", name="View").click()
# Open map and go to edit mode with peer B
peerB = new_page("Page B")
peerB.goto(peerA.url)
peerB.get_by_role("button", name="Edit").click()
# Create a marker from peerA
markersA = peerA.locator(".leaflet-marker-pane > div")
markersB = peerB.locator(".leaflet-marker-pane > div")
expect(markersA).to_have_count(0)
expect(markersB).to_have_count(0)
# Add a marker from peer A
peerA.get_by_role("button", name="Edit").click()
peerA.get_by_title("Draw a marker").click()
peerA.locator("#map").click(position={"x": 220, "y": 220})
expect(markersA).to_have_count(1)
expect(markersB).to_have_count(1)
# Save and quit edit mode again
with peerA.expect_response(re.compile("./datalayer/create/.*")):
peerA.get_by_role("button", name="Save").click()
peerA.get_by_role("button", name="View").click()
expect(markersA).to_have_count(1)
expect(markersB).to_have_count(1)
peerA.wait_for_timeout(500)
expect(markersA).to_have_count(1)
expect(markersB).to_have_count(1)
# Peer B should not be in state dirty
expect(peerB.get_by_role("button", name="View")).to_be_visible()
expect(peerB.get_by_role("button", name="Cancel edits")).to_be_hidden()
# Add a marker from peer B
peerB.get_by_title("Draw a marker").click()
peerB.locator("#map").click(position={"x": 200, "y": 200})
expect(markersB).to_have_count(2)
expect(markersA).to_have_count(1)
with peerB.expect_response(re.compile("./datalayer/update/.*")):
peerB.get_by_role("button", name="Save").click()
expect(markersB).to_have_count(2)
expect(markersA).to_have_count(1)
peerA.get_by_role("button", name="Edit").click()
expect(markersA).to_have_count(2)
expect(markersB).to_have_count(2)

View file

@ -126,10 +126,6 @@ async def join_and_listen(
try: try:
async for raw_message in websocket: async for raw_message in websocket:
if raw_message == "ping":
await websocket.send("pong")
continue
# recompute the peers list at the time of message-sending. # recompute the peers list at the time of message-sending.
# as doing so beforehand would miss new connections # as doing so beforehand would miss new connections
other_peers = connections.get_other_peers(websocket) other_peers = connections.get_other_peers(websocket)
@ -196,7 +192,4 @@ def run(host: str, port: int):
logging.debug(f"Waiting for connections on {host}:{port}") logging.debug(f"Waiting for connections on {host}:{port}")
await asyncio.Future() # run forever await asyncio.Future() # run forever
try: asyncio.run(_serve())
asyncio.run(_serve())
except KeyboardInterrupt:
print("Closing WebSocket server")