Quickstart
A WebSocket endpoint that serves a live tasks table, and a browser page that subscribes to it.
Server
With axum and PostgreSQL. Any WebSocket server works: feed incoming messages to client.consume and send what comes out of the receiver.
[dependencies]
axum = { version = "0.8", features = ["ws"] }
owservable-core = { git = "https://github.com/owservable/owservable-rs" }
owservable-postgres = { git = "https://github.com/owservable/owservable-rs" }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
use std::sync::Arc;
use axum::Router;
use axum::extract::State;
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
use axum::response::Response;
use axum::routing::get;
use owservable_core::{Client, ConnectionManager, Error, Registry};
use owservable_postgres::Postgres;
struct Session; // authentication hooks; all optional
impl ConnectionManager for Session {}
#[tokio::main]
async fn main() -> Result<(), Error> {
let db = Postgres::connect("host=localhost user=app dbname=app").await?;
let mut registry = Registry::default();
registry.register("tasks", db.watch("tasks").await?);
let app = Router::new().route("/ws", get(upgrade)).with_state(Arc::new(registry));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
async fn upgrade(ws: WebSocketUpgrade, State(registry): State<Arc<Registry>>) -> Response {
ws.on_upgrade(|socket| serve(socket, registry))
}
async fn serve(mut socket: WebSocket, registry: Arc<Registry>) {
let (mut client, mut outgoing) = Client::new(registry, Arc::new(Session));
loop {
tokio::select! {
Some(message) = outgoing.recv() => {
if socket.send(Message::Text(message.to_string().into())).await.is_err() {
break;
}
}
incoming = socket.recv() => match incoming {
Some(Ok(Message::Text(text))) => {
if let Ok(message) = serde_json::from_str(&text) {
let _ = client.consume(message).await;
}
}
Some(Ok(_)) => {}
_ => break,
},
}
}
client.disconnected().await;
}
For MongoDB use MongoDb::connect(uri, "app").await? and db.watch("tasks", &[]); for SQLite, Sqlite::open("app.db", Duration::from_millis(250))? and db.watch("tasks")?.
TypeScript v3, with ws, MongoDB and a mongoose model:
import {WebSocketServer} from 'ws';
import {OwservableClient, BackendRegistry} from '@owservable/core';
import {MongoDBConnector, MongoBackend} from '@owservable/mongodb';
import Task from './models/task.js'; // a mongoose model
await MongoDBConnector.init('mongodb://localhost:27017/app?replicaSet=rs0');
BackendRegistry.register('tasks', new MongoBackend(Task));
const session = {user: () => null, connected() {}, ping() {}, location() {}, disconnected() {}, checkSession: async () => null};
new WebSocketServer({port: 3000, path: '/ws'}).on('connection', (socket) => {
const client = new OwservableClient(session);
client.subscribe((message) => socket.send(JSON.stringify(message)));
socket.on('message', (data) => client.consume(JSON.parse(String(data))));
socket.on('close', () => client.disconnected());
});
Browser
No client library needed:
const socket = new WebSocket('ws://localhost:3000/ws');
socket.onopen = () => socket.send(JSON.stringify({
type: 'subscribe', target: 'openTasks', scope: 'many', observe: 'tasks',
config: {query: {status: 'open'}, sort: {id: 1}}
}));
socket.onmessage = ({data}) => {
const message = JSON.parse(data);
if (message.type === 'ping') socket.send(JSON.stringify({type: 'pong', id: message.id}));
if (message.type === 'update' && message.target === 'openTasks') render(message.payload.openTasks);
};
render runs once with the current open tasks, then again whenever a task is added, closed or edited, by this server or anything else writing to the database.
Next: the protocol covers paging, counts, totals, incremental lists and sessions.