feat(sync): Add a enableSync option.

This changes how the syncEngine works. At the moment, it's always
instanciated, even if no syncing is configured. It just does nothing.

This is to avoid doing `if (engine) engine.update()` calls everywhere
we use it.

You now need to `start()` and `stop()` it.
This commit is contained in:
Alexis Métaireau 2024-04-30 17:22:33 +02:00
parent 9a74cc370c
commit 5e692d2280
8 changed files with 60 additions and 28 deletions

View file

@ -1,8 +1,9 @@
import { translate } from './i18n.js' import { translate } from './i18n.js'
// Possible impacts // Possible impacts
// ['ui', 'data', 'limit-bounds', 'datalayer-index', 'remote-data', 'background'] // ['ui', 'data', 'limit-bounds', 'datalayer-index', 'remote-data', 'background' 'sync']
// This is sorted alphabetically
export const SCHEMA = { export const SCHEMA = {
browsable: { browsable: {
impacts: ['ui'], impacts: ['ui'],
@ -14,6 +15,12 @@ export const SCHEMA = {
label: translate('Do you want to display a caption bar?'), label: translate('Do you want to display a caption bar?'),
default: false, default: false,
}, },
captionControl: {
type: Boolean,
nullable: true,
label: translate('Display the caption control'),
default: true,
},
captionMenus: { captionMenus: {
type: Boolean, type: Boolean,
impacts: ['ui'], impacts: ['ui'],
@ -184,7 +191,6 @@ export const SCHEMA = {
type: Boolean, type: Boolean,
impacts: ['ui'], impacts: ['ui'],
}, },
interactive: { interactive: {
type: Boolean, type: Boolean,
impacts: ['data'], impacts: ['data'],
@ -373,12 +379,6 @@ export const SCHEMA = {
impacts: ['ui'], impacts: ['ui'],
label: translate('Allow scroll wheel zoom?'), label: translate('Allow scroll wheel zoom?'),
}, },
captionControl: {
type: Boolean,
nullable: true,
label: translate('Display the caption control'),
default: true,
},
searchControl: { searchControl: {
type: Boolean, type: Boolean,
impacts: ['ui'], impacts: ['ui'],
@ -437,6 +437,13 @@ export const SCHEMA = {
inheritable: true, inheritable: true,
default: true, default: true,
}, },
syncEnabled: {
type: Boolean,
impacts: ['sync', 'ui'],
label: translate('Enable real-time collaboration'),
helpEntries: 'sync',
default: false,
},
tilelayer: { tilelayer: {
type: Object, type: Object,
impacts: ['background'], impacts: ['background'],

View file

@ -8,16 +8,31 @@ import {
} from './updaters.js' } from './updaters.js'
export class SyncEngine { export class SyncEngine {
constructor(map, webSocketURI, authToken) { constructor(map) {
this.map = map this.map = map
this.receiver = new MessagesDispatcher(this.map) this.receiver = new MessagesDispatcher(this.map)
this._initialize()
}
_initialize() {
this.transport = undefined
const noop = () => undefined
// by default, all operations do nothing, until the engine is started.
this.upsert = this.update = this.delete = noop
}
start(webSocketURI, authToken) {
this.transport = new WebSocketTransport(webSocketURI, authToken, this.receiver) this.transport = new WebSocketTransport(webSocketURI, authToken, this.receiver)
this.sender = new MessagesSender(this.transport) this.sender = new MessagesSender(this.transport)
this.upsert = this.sender.upsert.bind(this.sender) this.upsert = this.sender.upsert.bind(this.sender)
this.update = this.sender.update.bind(this.sender) this.update = this.sender.update.bind(this.sender)
this.delete = this.sender.delete.bind(this.sender) this.delete = this.sender.delete.bind(this.sender)
} }
stop() {
if (this.transport) this.transport.close()
this._initialize()
}
} }
export class MessagesDispatcher { export class MessagesDispatcher {

View file

@ -19,4 +19,8 @@ export class WebSocketTransport {
let encoded = JSON.stringify(message) let encoded = JSON.stringify(message)
this.websocket.send(encoded) this.websocket.send(encoded)
} }
close() {
this.websocket.close()
}
} }

View file

@ -1186,10 +1186,8 @@ U.FormBuilder = L.FormBuilder.extend({
L.FormBuilder.prototype.setter.call(this, field, value) L.FormBuilder.prototype.setter.call(this, field, value)
this.obj.isDirty = true this.obj.isDirty = true
if ('render' in this.obj) this.obj.render([field], this) if ('render' in this.obj) this.obj.render([field], this)
console.log('setter', field, value)
const { subject, metadata, engine } = this.obj.getSyncMetadata() const { subject, metadata, engine } = this.obj.getSyncMetadata()
console.log('metadata', metadata) if (engine) engine.update(subject, metadata, field, value)
engine.update(subject, metadata, field, value)
}, },
finish: function () { finish: function () {

View file

@ -248,11 +248,16 @@ U.Map = L.Map.extend({
this.backup() this.backup()
this.initContextMenu() this.initContextMenu()
this.on('click contextmenu.show', this.closeInplaceToolbar) this.on('click contextmenu.show', this.closeInplaceToolbar)
this.sync = new U.SyncEngine(this)
Promise.resolve(this.initSyncEngine()) Promise.resolve(this.initSyncEngine())
}, },
initSyncEngine: async function () { initSyncEngine: async function () {
console.log('this.options.syncEnabled', this.options.syncEnabled)
if (this.options.syncEnabled != true) {
this.sync.stop()
} else {
// Get the authentication token from the server // Get the authentication token from the server
// And pass it to the sync engine. // And pass it to the sync engine.
// FIXME: use `this.urls` // FIXME: use `this.urls`
@ -260,7 +265,8 @@ U.Map = L.Map.extend({
`/map/${this.options.umap_id}/ws-token/` `/map/${this.options.umap_id}/ws-token/`
) )
if (!error) { if (!error) {
this.sync = new U.SyncEngine(this, 'ws://localhost:8001', response.token) this.sync.start('ws://localhost:8001', response.token)
}
} }
}, },
@ -294,6 +300,8 @@ U.Map = L.Map.extend({
case 'bounds': case 'bounds':
this.handleLimitBounds() this.handleLimitBounds()
break break
case 'sync':
this.initSyncEngine()
} }
} }
}, },
@ -1036,7 +1044,7 @@ U.Map = L.Map.extend({
formData.append('center', JSON.stringify(this.geometry())) formData.append('center', JSON.stringify(this.geometry()))
formData.append('settings', JSON.stringify(geojson)) formData.append('settings', JSON.stringify(geojson))
const uri = this.urls.get('map_save', { map_id: this.options.umap_id }) const uri = this.urls.get('map_save', { map_id: this.options.umap_id })
const [data, response, error] = await this.server.post(uri, {}, formData) const [data, _, error] = await this.server.post(uri, {}, formData)
// FIXME: login_required response will not be an error, so it will not // FIXME: login_required response will not be an error, so it will not
// stop code while it should // stop code while it should
if (!error) { if (!error) {
@ -1538,7 +1546,7 @@ U.Map = L.Map.extend({
if (!this.editEnabled) return if (!this.editEnabled) return
if (this.options.editMode !== 'advanced') return if (this.options.editMode !== 'advanced') return
const container = L.DomUtil.create('div', 'umap-edit-container'), const container = L.DomUtil.create('div', 'umap-edit-container'),
metadataFields = ['options.name', 'options.description'], metadataFields = ['options.name', 'options.description', 'options.syncEnabled'],
title = L.DomUtil.create('h3', '', container) title = L.DomUtil.create('h3', '', container)
title.textContent = L._('Edit map details') title.textContent = L._('Edit map details')
const builder = new U.FormBuilder(this, metadataFields, { const builder = new U.FormBuilder(this, metadataFields, {

View file

@ -60,6 +60,9 @@ U.Layer.Cluster = L.MarkerClusterGroup.extend({
initialize: function (datalayer) { initialize: function (datalayer) {
this.datalayer = datalayer this.datalayer = datalayer
if (!U.Utils.isObject(this.datalayer.options.cluster)) {
this.datalayer.options.cluster = {}
}
const options = { const options = {
polygonOptions: { polygonOptions: {
color: this.datalayer.getColor(), color: this.datalayer.getColor(),
@ -74,10 +77,6 @@ U.Layer.Cluster = L.MarkerClusterGroup.extend({
L.MarkerClusterGroup.prototype.initialize.call(this, options) L.MarkerClusterGroup.prototype.initialize.call(this, options)
this._markerCluster = U.MarkerCluster this._markerCluster = U.MarkerCluster
this._layers = [] this._layers = []
if (!U.Utils.isObject(this.datalayer.options.cluster)) {
this.datalayer.options.cluster = {}
}
}, },
onRemove: function (map) { onRemove: function (map) {

View file

@ -109,6 +109,7 @@ U.MapPermissions = L.Class.extend({
{ handler: 'ManageEditors', label: L._("Map's editors") }, { handler: 'ManageEditors', label: L._("Map's editors") },
]) ])
} }
const builder = new U.FormBuilder(this, fields) const builder = new U.FormBuilder(this, fields)
const form = builder.build() const form = builder.build()
container.appendChild(form) container.appendChild(form)

View file

@ -51,7 +51,7 @@ class OperationMessage(BaseModel):
subject: str = Literal["map", "layer", "feature"] subject: str = Literal["map", "layer", "feature"]
metadata: Optional[dict] = None metadata: Optional[dict] = None
key: Optional[str] = None key: Optional[str] = None
value: Optional[str | bool | int | GeometryValue] = None value: Optional[str | bool | int | GeometryValue | Geometry] = None
async def join_and_listen( async def join_and_listen(