Quick and dirty network synced vue state
For some apps it can be useful to have the state server-side and sync it to the clients live using websockets.
One option is to just send the full state everytime it has changed, this has some disadvantages though
- overhead: we send the full state even if just one property has changed
- reactivity: having the state as a ref or using Object.assign can have challenging side-effects regarding vue reactivity
- Object.assign does not delete properties
- Object.assign can destroy deeply nested objects which causes nasty referential loss behaviour that is hard to debug
- traceability: multiple changes since the last state are compressed into one change -if an array element is deleted and a new element is added in the same state update, a watcher for the array length would not be triggered if the full state is replaced instead of many, single assignments / updates (a delete and an add operation)
So a more selective update approach is to only send and change the stuff that has changed using fast-json-patch
This way, properties in the object are modified minimally invasive, just as if you modified this property at it's nesting level, as one single change instead of basically destroying and rebuilding the full reactivity tree.
Server
The server is a nodejs express server with socket.io
import express from "express";
import { Server as SocketIoServer } from "socket.io";
const app = express();
const server = app.listen(MAIN_PORT, () => {
console.log("express started on port " + MAIN_PORT);
});
const io = new SocketIoServer(server);
This is the main class with the update loop on the server
import { compare } from 'fast-json-patch/index.mjs';
import { createHash } from 'crypto';
export default class Main extends Serializable {
constructor() {
super();
this.last_state = {};
this.last_state_hash = null;
this.example = {foo: "bar"};
this.test = true;
}
getSerializable() {
return ["example", "test"];
}
init() {
setInterval(() => {
this.sendState();
}, 100);
//when a client connects, send it the full current state
io.on("connection", socket => {
socket.emit("state", this.last_state);
});
}
sendState() {
//serialize "this", triggering toJSON recursively
let state_string = JSON.stringify(this);
let hash = createHash("sha1").update(state_string).digest("hex");
//check if state has changed
if (this.last_state_hash === hash) return;
let state = JSON.parse(state_string);
let diff = compare(this.last_state, state);
this.last_state = state;
this.last_state_hash = hash;
//send state changes to all clients
io.emit("state-diff", diff);
}
}
Serializable
is just a little helper class that only serializes properties returned by this.getSerializable
when being passed through JSON.stringify
. This way last_state
and last_state_hash
are not part of the sync, only example
and test
export default class Serializable {
toJSON() {
const props = {};
this.getSerializable().forEach(key => {
props[key] = this[key];
});
return props;
}
}
The json diff library also includes an "observe" feature but its essentially just a setInterval loop that runs as fast as possible, so we can just leave it like it is and have more control over it.
Client
This is the client side "receiver" state
import { io } from "socket.io-client";
import { reactive } from "vue";
import { applyReducer } from "fast-json-patch/index.mjs";
const state = reactive({
loaded: false
});
socket.on("state", initial => {
Object.assign(state, initial);
state.loaded = true;
})
socket.on("state-diff", patches => {
patches.reduce(applyReducer, state);
})
export default state;
And that's it, quick and dirty