Concepts / Offline and merging
Offline and merging
Two copies of a Yjs document merge whatever each did while apart: each sends what the other is missing, and both end up with the same content, in any order. That's all reconnecting is.
import * as Y from '@y/y';
/**
* Two copies of one document edit apart, then exchange what the other is
* missing. Each sends only the updates past the other's state vector, and
* both end up with the same content, whatever order the updates arrive in.
*/
export function editApartThenMerge() {
const you = new Y.Doc();
const ana = new Y.Doc();
you.get('notes').insert(0, 'Bring snacks.');
Y.applyUpdate(ana, Y.encodeStateAsUpdate(you)); // both start from the same text
// Offline: neither sees the other's edit yet.
you.get('notes').insert(0, 'Friday: ');
ana.get('notes').insert(13, ' And plates.');
// Back online: sync step 1 sends a state vector, step 2 answers with the difference.
const toAna = Y.encodeStateAsUpdate(you, Y.encodeStateVector(ana));
const toYou = Y.encodeStateAsUpdate(ana, Y.encodeStateVector(you));
Y.applyUpdate(ana, toAna);
Y.applyUpdate(you, toYou);
return [you.get('notes').toString(), ana.get('notes').toString()];
}Work offline
jot makes this something you can try: ⋯ → Work offline, or a click on the status pill, closes the note's socket and keeps it editable. The pill counts what's waiting ("Offline · 3 edits waiting") until you press Reconnect; once the server has confirmed every edit, it says "Back online · merged" for a few seconds. Open the note in two windows, take one offline, type in both, and reconnect.
import type { WebsocketProvider } from '@y/websocket';
import type * as Y from '@y/y';
/**
* Work offline: close the socket and keep editing. The document keeps every
* local transaction; reconnecting runs the sync steps, and both sides merge.
*/
export function workOffline(provider: WebsocketProvider, doc: Y.Doc) {
let waiting = 0;
const count = (_update: Uint8Array, origin: unknown) => {
if (origin !== provider) waiting++; // a local edit the server hasn't seen
};
provider.disconnect();
doc.on('update', count);
return {
edits: () => waiting,
reconnect() {
doc.off('update', count);
provider.connect();
},
};
}What happens on reconnect
- The socket reopens. On a lost network the provider retries with backoff.
- The server sends sync step 1: its state vector, a compact summary of what it has.
- The client answers with sync step 2: every update past that vector, its offline edits included. It sends its own step 1, and the server answers the same way.
- The server applies the client's updates and sends them to every socket, the sender's included:
@y/websockettakes its own updates coming back as confirmation.
When the browser says the network is back, jot doesn't wait for the next retry, unless you chose to work offline:
import type { WebsocketProvider } from '@y/websocket';
/**
* The provider retries on its own with backoff. When the browser says the
* network is back, skip the wait and reconnect at once, unless the person
* chose to work offline; sync step 1 and 2 then exchange whatever either side
* wrote while apart.
*/
export function reconnectWhenOnline(provider: WebsocketProvider, workOffline: () => boolean) {
const onOnline = () => {
if (!provider.wsconnected && !workOffline()) {
provider.disconnect();
provider.connect();
}
};
window.addEventListener('online', onOnline);
return () => window.removeEventListener('online', onOnline);
}Where offline edits live
In memory. There's no Yjs 14 IndexedDB provider yet, so a note's unsynced edits stay in the page until the socket is back, and closing its tab while offline loses them. A full note is the one exception to "nothing is lost": an edit that would pass 2 MB is refused when it arrives.