feat(sync): sync features and map properties

Synced objects now expose different methods, such as:
- `getSyncEngine` which returns the location of the sync object.
- `getMetadata` which returns the associated metadata with the object.

Hooks have been added when features are created or changed, so the
changes can be synced with other peers.
This commit is contained in:
Alexis Métaireau 2024-04-19 17:15:12 +02:00
parent f255c3c8a5
commit 698c926997
6 changed files with 67 additions and 7 deletions

View file

@ -53,7 +53,7 @@ export class MessagesDispatcher {
dispatch({ kind, payload }) {
console.log(kind, payload)
if (kind == 'sync-protocol') {
if (kind == 'operation') {
let updater = this.getUpdater(payload.subject, payload.metadata)
updater.applyMessage(payload)
}
@ -74,7 +74,7 @@ export class MessagesSender {
}
send(message) {
this._transport.send('sync-protocol', message)
this._transport.send('operation', message)
}
upsert(subject, metadata, value) {

View file

@ -1263,6 +1263,7 @@ U.Editable = L.Editable.extend({
this.on('editable:drawing:commit', function (e) {
e.layer.isDirty = true
if (this.map.editedFeature !== e.layer) e.layer.edit(e)
e.layer.onCommit()
})
this.on('editable:editing', (e) => {
const layer = e.layer

View file

@ -1,6 +1,44 @@
U.FeatureMixin = {
staticOptions: { mainColor: 'color' },
getSyncMetadata: function () {
return {
subject: 'feature',
metadata: {
id: this.id,
layerId: this.datalayer?.id || null,
featureType: this.getClassName(),
},
}
},
getSyncEngine: function () {
// FIXME use a get property / defineProperty
return this.map.sync
},
onCommit: function () {
this.map.sync.upsert(this.getSyncSubject(), this.getSyncMetadata(), {
geometry: this.getGeometry(),
})
},
getGeometry: function () {
return this.toGeoJSON().geometry
},
syncUpdatedProperties: function (properties) {
if ('latlng'.includes(properties)) {
const { subject, metadata } = this.getSyncMetadata()
this.map.sync.update(subject, metadata, 'geometry', this.getGeometry())
}
},
syncDelete: function () {
let { subject, metadata } = this.getSyncMetadata()
this.map.sync.delete(subject, metadata)
},
initialize: function (map, latlng, options) {
this.map = map
if (typeof options === 'undefined') {
@ -241,12 +279,15 @@ U.FeatureMixin = {
return false
},
del: function () {
del: function (fromSync) {
this.isDirty = true
this.map.closePopup()
if (this.datalayer) {
this.datalayer.removeLayer(this)
this.disconnectFromDataLayer(this.datalayer)
// Do not relay the event if we received it.
if (!fromSync) this.syncDelete()
}
},
@ -602,6 +643,7 @@ U.Marker = L.Marker.extend({
function (e) {
this.isDirty = true
this.edit(e)
this.syncUpdatedProperties(['latlng'])
},
this
)

View file

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

View file

@ -249,10 +249,10 @@ U.Map = L.Map.extend({
this.initContextMenu()
this.on('click contextmenu.show', this.closeInplaceToolbar)
Promise.resolve(this.getSyncEngine())
Promise.resolve(this.initSyncEngine())
},
getSyncEngine: async function () {
initSyncEngine: async function () {
// Get the authentication token from the server
// And pass it to the sync engine.
const [response, _, error] = await this.server.get('/map/2/ws-token/')
@ -261,6 +261,16 @@ U.Map = L.Map.extend({
}
},
getSyncMetadata: function () {
return {
subject: 'map',
}
},
getSyncEngine: function () {
return this.sync
},
render: function (fields) {
let impacts = U.Utils.getImpactsFromSchema(fields)

View file

@ -2,7 +2,7 @@
import asyncio
from collections import defaultdict
from typing import Literal
from typing import Literal, Optional
import django
import websockets
@ -32,11 +32,15 @@ class JoinMessage(BaseModel):
token: str
# FIXME better define the different messages
# to ensure only relying valid ones.
class OperationMessage(BaseModel):
kind: str = "operation"
verb: str = Literal["upsert", "update", "delete"]
subject: str = Literal["map", "layer", "feature"]
metadata: dict
data: dict
key: str
value: Optional[str]
async def join_and_listen(