offline-sync-lite
v0.1.1
Published
Lightweight offline-first sync SDK for browser clients using IndexedDB.
Maintainers
Readme
offline-sync-lite
Lightweight offline-first sync SDK for Next.js. Enables seamless data synchronization on unstable networks using IndexedDB for persistence. Zero heavy dependencies, TypeScript-free, browser-native.
- 📦 Tiny: ~18 KB minified (zero external deps)
- 🔄 Offline-first: Queue operations locally, replay when online
- 🤝 Conflict resolution: Last-write-wins + custom resolver hook
- 📊 Metrics: Sync success rate, latency, retry count, queued ops
- ⚡ Auto-sync: Periodic intervals + online event detection
- 🎯 Ergonomic API: Designed for Next.js client components
Installation
npm install offline-sync-liteQuick Start
Next.js Client Component
'use client';
import { createSyncClient } from 'offline-sync-lite';
import { useEffect, useState } from 'react';
export default function TasksPage() {
const [tasks, setTasks] = useState([]);
const [metrics, setMetrics] = useState(null);
const client = createSyncClient({
apiUrl: '/api', // Base URL for REST endpoints
resourceName: 'tasks', // Resource collection name
syncIntervalMs: 10000, // Auto-sync every 10s (optional)
maxRetries: 3, // Retry failed ops up to 3 times
backoffBaseMs: 500, // Exponential backoff base (500ms)
conflictResolver: undefined, // Optional custom conflict resolver
onEvent: (evt) => { // Optional event listener
if (evt.type === 'sync_done') {
console.log('Sync completed:', evt);
}
}
});
useEffect(() => {
// Subscribe to local changes
const unsub = client.subscribe((items) => {
setTasks(items);
client.getMetrics().then(setMetrics);
});
client.startAutoSync();
client.list().then(setTasks);
return () => {
client.stopAutoSync();
unsub();
};
}, []);
return (
<div>
<h1>Tasks</h1>
{metrics && (
<div style={{ fontSize: '0.85rem', color: '#666' }}>
Sync: {(metrics.syncSuccessRate * 100).toFixed(0)}% |
Queue: {metrics.queuedOpsCount} |
Conflicts: {metrics.conflictsDetected}
</div>
)}
<button onClick={() => client.create({ id: crypto.randomUUID(), data: { title: 'New' } })}>
Add
</button>
<ul>
{tasks.map((t) => (
<li key={t.id}>
{t.data?.title}
<button onClick={() => client.remove(t.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}API Reference
createSyncClient(config)
Returns a client instance with the following methods:
create(doc): Promise<record>
Create a new record locally and queue for sync.
await client.create({
id: 'task-1',
data: { title: 'Build an app', status: 'open' }
});update(id, patch): Promise<record>
Update an existing record. The patch is merged into local data.
await client.update('task-1', { status: 'in-progress' });remove(id): Promise<boolean>
Delete a record locally and queue deletion.
await client.remove('task-1');get(id), list(): Read from local IndexedDB.
const record = await client.get('task-1');
const allRecords = await client.list();subscribe(fn): unsubscribe
Listen to local changes (create/update/remove/pull).
const unsub = client.subscribe((items) => console.log(items));
unsub(); // Stop listeningsyncNow(): Promise<{ok, pushedOps, failedOps, appliedUpdates, durationMs}>
Manually trigger sync: push queued ops, pull remote updates, resolve conflicts.
pauseSync() / resumeSync()
NEW! Temporarily pause and resume sync operations.
// Pause sync (stops auto-sync, blocks syncNow)
client.pauseSync();
// Resume sync (restarts auto-sync if configured, triggers immediate sync)
client.resumeSync();startAutoSync() / stopAutoSync()
Enable/disable periodic syncing + online event detection.
getMetrics(): Promise<metrics>
{
syncSuccessRate: 0.95, // (0–1) success rate
avgSyncLatencyMs: 245, // Average duration
retryCount: 2, // Total retries
queuedOpsCount: 3, // Pending ops
conflictsDetected: 1 // Total conflicts
}Conflict Resolution
Records have updatedAt (ISO string) and serverVersion (monotonic counter). Default uses last-write-wins:
- Prefer remote if
remoteTime > localTime - Tiebreaker: higher
serverVersionwins - Custom: provide your own resolver
const resolver = (local, remote) => {
// Return the version to keep
if (local?.data?.priority > remote?.data?.priority) return local;
return remote;
};
const client = createSyncClient({
apiUrl: '/api',
resourceName: 'tasks',
conflictResolver: resolver
});Events during sync:
onEvent: (evt) => {
if (evt.type === 'conflict') {
console.log('Conflict for', evt.id, '→ applied', evt.merged);
}
}Auto-Sync & Online Detection
Auto-sync triggers on:
- Interval: Every
syncIntervalMsmilliseconds - Online event: When device reconnects (browser
onlineevent) - Manual:
client.syncNow()
Operations are immediately queued locally (IndexedDB persists across restarts).
Metrics
Passively collected throughout lifecycle:
| Metric | Meaning |
|--------|---------|
| syncSuccessRate | Percentage of syncs that completed (0–1) |
| avgSyncLatencyMs | Average of last 50 successful syncs |
| retryCount | Total operation retries |
| queuedOpsCount | Operations pending sync |
| conflictsDetected | Total conflicts resolved |
Server API Contract
Implement these REST endpoints. Records: {id, data, updatedAt, serverVersion}.
POST /api/{resourceName}
Create. Response: full record with serverVersion.
POST /api/{resourceName}/batch (Optional, NEW!)
Batch operations. For optimal performance, implement this endpoint to handle multiple operations in one request:
// Request
{ operations: [{ type: 'create', id: '1', payload: {...} }, ...] }
// Response
{ results: [{ success: true, record: {...} }, ...] }If not implemented, the SDK automatically falls back to individual operations.
PATCH /api/{resourceName}/{id}
Update. On conflict (409), include {error, remoteRecord} in response.
DELETE /api/{resourceName}/{id}
Delete. Response: {ok: true}.
GET /api/{resourceName}?since={ISO8601}
List optionally since timestamp. Response: {records: [...], serverTime: ISO8601}.
How It Works
- Local ops: Immediately persist to IndexedDB and queue
- Coalesce: Merge multiple ops (create→update→single create)
- Push: Replay queued ops with exponential backoff retry
- Handle conflicts: 409 → resolve → retry
- Pull: Fetch remote changes since last sync
- Merge: Apply newer remote records; resolve conflicts
- Notify: Emit events; update subscribers
Limitations
- Single resource per client: One
resourceNameper instance (create multiple clients for multiple resources) - Browser-only: Requires IndexedDB, fetch, navigator.onLine
- Last-write-wins default: Not application-semantic; use custom resolver for domain logic
- No transactions: Individual ops; no multi-doc ACID
- No auth in SDK: Implement in server middleware or
onEventhandler
Performance
- Bundle: ~18 KB minified, zero deps
- IndexedDB: Efficient prefix ranges for listing
- Sync: Typical 100–500 ms on good networks
- Coalescing: Reduces payload 50–80% for rapid updates
- Batch sync: 10× faster when server implements batch endpoint (10 ops in 1 request vs 10 requests)
Browser Support
Requires IndexedDB, fetch, ES2020. Tested on Chrome 90+, Firefox 88+, Safari 14+, Edge 90+.
License
MIT
