Skip to content

wrpc transport ​

wrpc is the transport syncom is built to run on: WebSocket RPC with subscriptions that resume, reconnection with backoff, rooms and binary streams — everything the sync protocol leaves to the link. The adapter lives in the @alexify/syncom/wrpc subpath and takes a WrpcClient you construct; syncom never imports wrpc, so its dependency count stays at zero and wrpc stays on your version.

bash
npm install @alexify/syncom @alexify/wrpc

(syncom is not on npm yet — see Installation.)

Hosting a node ​

storage.sync.unit() returns a plain object of procedures shaped for defineRouter. Mount it under syncom:

js
const { Server, defineRouter } = require('@alexify/wrpc');
const { open } = require('@alexify/syncom');

const storage = await open({ path: 'data' });

const router = defineRouter({
  syncom: storage.sync.unit({
    access: 'public',
    authorize: (context, hello) => hello.token === process.env.SYNC_TOKEN,
  }),
});

const server = new Server({ router, host: '0.0.0.0', port: 8000, protocol: 'http' });
await server.listen();

access defaults to wrpc's own default, 'session', which refuses every peer until one has a session. Either open it with 'public' and gate with authorize, or keep sessions and sign peers in through wrpc's authenticate hook — context.session is available inside authorize too.

Dialing a node ​

js
const { WrpcClient } = require('@alexify/wrpc');
const { open } = require('@alexify/syncom');
const { createWrpcLink } = require('@alexify/syncom/wrpc');

const storage = await open({
  path: 'data',
  sync: {
    connect: async (node) => createWrpcLink(await WrpcClient.connect(node.url)),
  },
});

await storage.sync.addNode({ url: 'ws://hub.example.com:8000/api', token: process.env.SYNC_TOKEN });

connect(node) is called once per registered node and again whenever the node is re-enabled, so every dial gets a fresh client. Keep wrpc's reconnect and heartbeat defaults: reconnecting is the client's job, and after it the subscription resumes from its lastEventId by itself — syncom only repeats its handshake to push what it wrote while offline.

A node can be both. The same storage can mount its unit on a server and dial other nodes; that is what makes a mesh.

In the browser ​

The subpath has no Node.js code, so the same two imports work in a bundle:

js
import { open } from '@alexify/syncom';
import { createWrpcLink } from '@alexify/syncom/wrpc';
import { WrpcClient } from '@alexify/wrpc';

const storage = await open({
  path: 'app',
  sync: { connect: async (node) => createWrpcLink(await WrpcClient.connect(node.url)) },
});
await storage.sync.addNode({ url: 'wss://hub.example.com/api', token });

A browser node dials the nodes it can reach, and a bootstrap arrives as a binary stream through wrpc's WrpcReadable. It hosts too: over a WebRTC link its unit sits on a wrpc PeerHost, and a peer's PeerLink.remote is a WrpcClient this adapter takes as it is — see WebRTC.

What the adapter does ​

createWrpcLink(client, { unit }):

  • call('syncom/x', args) → client.call('syncom/x', args) — no load() needed.
  • subscribe('syncom/changes', …) → client.use(SYNC_INTROSPECTION) (no round trip) then client.api.syncom.changes.subscribe(args, { lastEventId, … }). wrpc updates the handle's lastEventId from each packet before it delivers the value, and the adapter passes that id along with it.
  • stream(id) → client.getStream(id), the async-iterable WrpcReadable a streamed snapshot page arrives on.
  • open / close / reconnect events and close() are the client's own.

unit renames the router unit when you mounted it under something other than syncom. isSyncLink() is exported too, and SYNC_INTROSPECTION if you want to scaffold the unit yourself.

Tracing across the wire ​

The adapter adds nothing for tracing, and needs nothing: give wrpc's Server and WrpcClient their telemetry option and syncom its own, and a write on the dialing node, its syncom/push call, the handler on the host and the syncom.apply inside it are one trace — wrpc carries the context per packet, syncom's spans nest under whatever is active. See OpenTelemetry.

Rooms and presence ​

On hello the host joins the peer to syncom:<scope> for every scope it asked for, so wrpc's rooms and cluster describe the sync topology for free:

js
server.rooms.count('syncom:*');                            // peers on this instance
await server.cluster.fetchClients({ room: 'syncom:Doc' }); // across instances
server.cluster.disconnect({ room: 'syncom:*' });           // drop every peer

Delivery does not go through rooms: a room broadcast is at-most-once and cannot be resumed, which is exactly what the subscription provides.

Why no codec ​

wrpc's wire codec is a connection-wide, text-only setting your application owns. syncom's ops already travel packed (one positional array per op, no repeated keys), permessage-deflate compresses the rest, and the storage is JSON on both ends — there is no binary representation to preserve. Set a codec if your application wants one; syncom neither needs nor notices it.

Testing ​

The pattern from wrpc's own testing guide applies unchanged — port: 0, logger: false, teardown registered with t.after the moment the server is up. syncom's tests/wrpc.integration.test.js boots two servers and a dialer that way and is skipped, never failed, when @alexify/wrpc is not installed.

Released under the MIT License.