@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.

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 conflict event for custom resolution
  • Ephemeral change channel with no server-side retention
  • Collaborator online/offline presence via lobby
  • Local cache: getDocument() and getAllDocuments() are synchronous reads

How It Works

NoLagSync wraps the @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.

TopicPurposeReplay
changesDocument create, update, and delete operations with version metadataEphemeral

Installation

npm install @nolag/sync @nolag/js-sdk

Quick Start

import { NoLagSync } from '@nolag/sync'

const sync = new NoLagSync('your-access-token')
await sync.connect()

// 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')
})

API Reference

NoLagSync

MethodReturnsDescription
connect()Promise<void>Establish the WebSocket connection and join the lobby.
disconnect()voidClose the connection and leave the lobby.
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

EventPayloadDescription
connectednoneWebSocket connection established.
disconnectedreason: stringConnection closed.
reconnectednoneConnection restored; collection membership and presence are restored automatically.
errorerror: ErrorA transport or protocol error occurred.
collaboratorOnlinecollaborator: CollaboratorA collaborator joined the lobby.
collaboratorOfflinecollaborator: CollaboratorA collaborator left the lobby.

SyncRoom

MethodReturnsDescription
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 | undefinedRead 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

EventPayloadDescription
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.
syncednoneThe local cache is fully reconciled with the server state.
collaboratorJoinedcollaborator: CollaboratorA collaborator joined this collection.
collaboratorLeftcollaborator: CollaboratorA collaborator left this collection.