Real-time JS object synchronisation over SSE and WebSockets in Go and JavaScript (Node.js and browser)
- Simple API
- Synchronise any JSON marshallable struct in Go
- Synchronise any JSON stringifiable struct in Node
- Delta updates using JSONPatch (RFC6902)
- Supports Server-Sent Events (EventSource) and WebSockets
- SSE client-side poly-fill to fallback to long-polling in older browsers (IE8+).
- Generic
VMapandVSlicecontainers with automatic locking and push-on-write - Go client (
velox.Client[T]) for server-to-server sync
Server (Go)
type Foo struct {
velox.State
A, B int
}
foo := &Foo{}
http.Handle("/velox.js", velox.JS)
http.Handle("/sync", velox.SyncHandler(foo))
// make changes and push to all clients
foo.A = 42
foo.B = 21
foo.Push()Server (Node)
//syncable object
let foo = {
a: 1,
b: 2
};
//express server
let app = express();
//serve velox.js client library (assets/dist/velox.min.js)
app.get("/velox.js", velox.JS);
//serve velox sync endpoint for foo (adds $push method)
app.get("/sync", velox.handle(foo));
//make changes
foo.a = 42;
foo.b = 21;
//push to client
foo.$push();Client (Node and Browser)
// load script /velox.js
var foo = {};
var v = velox("/sync", foo);
v.onupdate = function() {
//foo.A === 42 and foo.B === 21
};Server API (Go)
Server API (Node)
velox.handle(object)function returnsv- Creates a new route handler for use with expressvelox.state(object)function returnsstate- Creates or restores a velox state from a given objectstate.handle(req, res)function returnsPromise- Handle the providedexpressrequest/response. Resolves on connection close. Rejects on any error.
Client API (Node and Browser)
velox(url, object)function returnsv- Creates a new SSE velox connectionvelox.sse(url, object)function returnsv- Creates a new SSE velox connectionvelox.ws(url, object)function returnsv- Creates a new WS velox connectionv.onupdate(object)function - Called when a server push is receivedv.onerror(err)function - Called when a connection error occursv.onconnect()function - Called when the connection is openedv.ondisconnect()function - When the handler declares no parameters (arity 0, the default), it is called once on each disconnect as a transition notification while velox continues to reconnect on its own with exponential backoff.v.ondisconnect(retry)function - When the handler declares aretryparameter (arity 1), velox suppresses its own backoff retries and instead invokes this handler on every connection close (including while offline), passing aretrytrigger so the caller controls reconnect timing (e.g. to drive a visible countdown). Callretry()to reconnect.v.onchange(bool)function - Called when the connection is opened or closedv.connectedbool - Denotes whether the connection is currently openv.wsbool - Denotes whether the connection is in web sockets modev.ssebool - Denotes whether the connection is in server-sent events mode
See this simple example/ and view it live here: https://velox.jpillora.com
Here is a screenshot from this example page, showing the messages arriving as either a full replacement of the object or just a delta. The server will send which ever is smaller.
VMap[K, V] and VSlice[V] are generic containers that automatically lock the
root struct and push changes to clients on every write operation. This removes
the need to manually call Lock/Unlock/Push when mutating map or slice
fields.
type App struct {
sync.RWMutex
velox.State
Settings velox.VMap[string, string] `json:"settings"`
Scores velox.VMap[string, int] `json:"scores"`
Logs velox.VSlice[string] `json:"logs"`
}
app := &App{}
http.Handle("/sync", velox.SyncHandler(app))
// Each call locks the RWMutex, mutates the data, and pushes (throttled).
// No manual Lock/Unlock/Push needed.
app.Settings.Set("theme", "dark")
app.Scores.Batch(func(data map[string]int) {
data["alice"] = 100
data["bob"] = 85
})
app.Logs.Append("server started")SyncHandler automatically binds all VMap/VSlice fields to the struct's
mutex and State pusher. On the client side, velox.Client[T] rebinds after
each update.
How locking works:
- Write methods (
Set,Delete,Append,Update,Batch,Clear) acquire the root struct'sLock(), mutate the data, callState.Push(), thenUnlock(). - Read methods (
Get,Len,Keys,Values,Snapshot,Range) useRLock()/RUnlock()when the root struct embedssync.RWMutex, allowing concurrent readers. Falls back toLock()/Unlock()forsync.Mutex. State.Push()is throttled (default 200ms) and non-blocking -- it spawns a goroutine that waits for the lock to be released, marshals the struct, computes a delta, and sends it to all connected clients. Rapid mutations are coalesced into fewer pushes.MarshalJSON/UnmarshalJSONon VMap/VSlice do not lock -- the parent already holds the lock during marshal.
VMap methods:
| Write (lock + push) | Read (rlock) |
|---|---|
Set(key, value) |
Get(key) (V, bool) |
Delete(key) |
Has(key) bool |
Update(key, func(*V)) bool |
Len() int |
Batch(func(map[K]V)) |
Keys() []K |
Clear() |
Values() []V |
Snapshot() map[K]V |
|
Range(func(K, V) bool) |
VSlice methods:
| Write (lock + push) | Read (rlock) |
|---|---|
Set([]V) |
Get() []V |
Append(values...) |
At(index) (V, bool) |
SetAt(index, value) bool |
Len() int |
DeleteAt(index) bool |
Range(func(int, V) bool) |
Update(index, func(*V)) bool |
|
Batch(func(*[]V)) |
|
Clear() |
- Object synchronization is one way (server to client) only.
- JS object properties beginning with
$will be ignored to play nice with Angular. - JS object with an
$applyfunction will automatically be called on each update to play nice with Angular.
Copyright © 2018 Jaime Pillora <dev@jpillora.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
