@nolag/sync
Real-time data sync with document CRUD, version tracking, and conflict resolution.
Overview
Sync structured data across clients in real time. Documents within a collection carry monotonically increasing version numbers and are merged using a last-writer-wins (LWW) strategy applied per field, so concurrent edits to different fields of the same document are automatically reconciled. When two clients update the same field simultaneously, a conflict event is emitted so your application can apply custom resolution logic if the default LWW behaviour is not appropriate. Collections group related documents and behave like lightweight database tables. Your app owns one core NoLag client and injects it into NoLagSync; the wrapper attaches its behaviour to that connection.
Key Features
- Real-time document create, update, and delete broadcast to all collection subscribers
- Per-field last-writer-wins merge with no full-document overwrites
- Automatic conflict detection with a
conflictevent for custom resolution - Ephemeral change channel with no server-side retention
- Collaborator online/offline presence via lobby
- Local cache:
getDocument()andgetAllDocuments()are synchronous reads
How It Works
NoLagSync attaches to an injected @nolag/js-sdk client and manages a lobby that tracks which collaborators are online. Calling joinCollection(name) returns a SyncRoom that subscribes to the changes topic. Every create, update, and delete operation is published to that topic so all subscribers receive it and update their local document cache. The SDK maintains a version counter per document and detects conflicts when an incoming update has a base version lower than the current local version. The app owns the socket lifecycle; the wrapper never opens or closes it.
| Topic | Purpose | Replay |
|---|---|---|
changes | Document create, update, and delete operations with version metadata | Ephemeral |
Installation
npm install @nolag/sync @nolag/js-sdkOne core NoLag client can back several wrapper SDKs at once, for example data sync, chat, and notify on a single socket, as long as each wrapper uses a distinct appName. Each wrapper attaches its handlers on construction and releases them with detach(), and never touches the socket itself. Your app owns connect() and disconnect().
Quick Start
import { NoLag } from '@nolag/js-sdk'
import { NoLagSync } from '@nolag/sync'
// The app owns one core client. In a browser, pass a token provider so the
// SDK can mint fresh short-lived client tokens from your backend.
const client = NoLag(async () => (await (await fetch('/api/nolag-token')).json()).token)
// Inject the client into the sync wrapper
const sync = new NoLagSync({ client, username: 'Alice' })
await client.connect() // the app owns the connection
await sync.ready() // wrapper setup complete
// Join a collection (analogous to a database table or folder)
const collection = await sync.joinCollection('tasks')
// Create a document (all collaborators see it instantly)
await collection.createDocument('task-001', {
title: 'Design new homepage',
status: 'todo',
assignee: 'alice',
priority: 1,
})
// Update specific fields (version is incremented automatically)
await collection.updateDocument('task-001', {
status: 'in-progress',
assignee: 'bob',
})
// Read a document from local cache (no network call)
const task = await collection.getDocument('task-001')
console.log(task?.version) // e.g. 2
// Read all documents in the collection
const all = await collection.getAllDocuments()
console.log(`${all.length} documents in collection`)
// Delete a document
await collection.deleteDocument('task-001')
// React to changes from other collaborators
collection.on('documentCreated', ({ id, data, version }) => {
console.log('New document:', id, data)
})
collection.on('documentUpdated', ({ id, fields, version }) => {
console.log('Updated:', id, fields)
})
collection.on('documentDeleted', ({ id }) => {
console.log('Deleted:', id)
})
// Handle conflicts (last-writer-wins per field, conflict emitted for custom resolution)
collection.on('conflict', ({ id, localVersion, remoteVersion, fields }) => {
console.warn('Conflict on', id, '- remote version wins unless resolved manually')
})
// Know when the local state is fully in sync with the server
collection.on('synced', () => {
console.log('Collection fully synced')
})
// Teardown: the wrapper releases its handlers; the app closes the socket.
sync.detach()
client.disconnect()API Reference
NoLagSync
Constructor Options
| Option | Type | Description |
|---|---|---|
client | NoLagSocket | Required. The injected core NoLag client the app owns and connects. |
userId | string | Stable user ID (auto-generated if omitted). |
username | string | Optional human-readable display name. |
metadata | Record<string, unknown> | Optional custom data attached to collaborator presence. |
appName | string | NoLag app for topic prefixes (default 'sync'). |
collections | string[] | Collections to pre-join once the wrapper is ready. |
debug | boolean | Enable wrapper debug logging (default false). |
| Method | Returns | Description |
|---|---|---|
ready() | Promise<void> | Resolves once wrapper setup completed. |
detach() | void | Release this wrapper's handlers and topics; terminal, never closes the socket. |
joinCollection(name) | Promise<SyncRoom> | Subscribe to a collection. Returns the room instance with a pre-populated local cache. |
leaveCollection(name) | Promise<void> | Unsubscribe from a collection and release its local cache. |
getCollaborators() | Collaborator[] | Return all collaborators currently online in the lobby. |
NoLagSync Events
| Event | Payload | Description |
|---|---|---|
connected | none | WebSocket connection established. |
disconnected | reason: string | Connection closed. |
reconnected | none | Connection restored; collection membership and presence are restored automatically. |
error | error: Error | A transport or protocol error occurred. |
collaboratorOnline | collaborator: Collaborator | A collaborator joined the lobby. |
collaboratorOffline | collaborator: Collaborator | A collaborator left the lobby. |
SyncRoom
| Method | Returns | Description |
|---|---|---|
createDocument(id, data) | Promise<Document> | Create a new document with an explicit ID and initial field values. Version starts at 1. |
updateDocument(id, fields) | Promise<Document> | Apply a partial field update. Merges with existing data; increments version. |
deleteDocument(id) | Promise<void> | Delete a document from the collection. Propagated to all subscribers. |
getDocument(id) | Document | undefined | Read a document from the local cache. Returns undefined if not found. |
getAllDocuments() | Document[] | Return all documents currently in the local cache for this collection. |
SyncRoom Events
| Event | Payload | Description |
|---|---|---|
documentCreated | { id, data, version, authorId } | A new document was created by any collaborator. |
documentUpdated | { id, fields, version, authorId } | A document's fields were updated by any collaborator. |
documentDeleted | { id, authorId } | A document was deleted by any collaborator. |
localChange | { id, type, data } | Fired when the local client applies a change (before server acknowledgment). |
conflict | { id, localVersion, remoteVersion, fields } | Concurrent write conflict detected. Remote version is applied; use this event to override. |
synced | none | The local cache is fully reconciled with the server state. |
collaboratorJoined | collaborator: Collaborator | A collaborator joined this collection. |
collaboratorLeft | collaborator: Collaborator | A collaborator left this collection. |