@sakib11/syncforge
v1.0.0
Published
A library for incremental and conflict-free data synchronization across multiple clients or databases. Supports CRDTs, timestamp-based merges, and offline-first workflows.
Maintainers
Readme
@sakib11/syncforge
A TypeScript library for incremental and conflict-free data synchronization across multiple clients or databases. Supports CRDTs, timestamp-based merges, and offline-first workflows.
Features
- Pluggable storage backends -- memory, filesystem, Redis, and Amazon S3
- Two conflict resolution strategies -- timestamp (last-writer-wins) and CRDT (field-level merge)
- Incremental sync -- only transfer records that changed since the last sync
- Batch processing -- large sync operations are chunked automatically
- Retry with exponential back-off -- failed syncs retry up to a configurable limit
- Auto-sync timer -- optional periodic sync on a configurable interval
- Typed event system -- subscribe to sync lifecycle, conflict, and record-level events
- Offline-first -- write locally, sync when connectivity is available
- Zero required dependencies -- Redis and S3 backends use optional peer dependencies
When to Use
This library is a data synchronization layer, not a database or UI framework. It runs anywhere Node.js runs -- frontend, backend, edge, or embedded. The core problem it solves is: multiple independent writers need to converge to a consistent state.
Offline-First Mobile / Desktop Apps
Users work without connectivity and sync when back online. The engine queues writes locally and merges seamlessly on reconnect.
Context: A field-service app where technicians inspect equipment at remote sites with no cell coverage. They log findings on a tablet, and everything syncs to the company server when they return to the office.
import { SyncEngine } from '@sakib11/syncforge';
// On the tablet -- works offline with filesystem persistence
const tablet = new SyncEngine({
clientId: 'technician-42',
backend: 'filesystem',
filesystemConfig: { directory: './local-data' },
conflictStrategy: 'crdt',
});
// Technician logs an inspection offline
await tablet.put('inspections', {
id: 'insp-001',
equipmentId: 'pump-7',
status: 'needs-repair',
notes: 'Bearing noise detected',
});
// Later, back at the office -- push changes to the server
const changes = await tablet.getChangesSince('inspections', lastSyncTimestamp);
const response = await fetch('/api/sync/inspections', {
method: 'POST',
body: JSON.stringify(changes),
});
const serverRecords = await response.json();
await tablet.sync('inspections', serverRecords);Multi-Device Sync
The same user edits data on multiple devices. Changes made on any device propagate to all others.
Context: A note-taking app where a user drafts notes on their phone during a commute and continues editing on their laptop at home.
// Phone instance
const phone = new SyncEngine({ clientId: 'phone', conflictStrategy: 'crdt' });
await phone.put('notes', { id: 'n1', title: 'Meeting Notes', body: 'Discuss Q3 targets' });
// Laptop instance
const laptop = new SyncEngine({ clientId: 'laptop', conflictStrategy: 'crdt' });
// Sync phone → laptop
const phoneRecords = await phone.getAll('notes');
await laptop.sync('notes', phoneRecords);
// User edits title on laptop, body on phone (different fields, no conflict)
await laptop.put('notes', { id: 'n1', title: 'Q3 Planning Notes', body: 'Discuss Q3 targets' });
await phone.put('notes', { id: 'n1', title: 'Meeting Notes', body: 'Discuss Q3 targets\n- Revenue goals' });
// Sync both ways -- CRDT preserves both field changes
const laptopRecords = await laptop.getAll('notes');
await phone.sync('notes', laptopRecords);
const updatedPhone = await phone.getAll('notes');
await laptop.sync('notes', updatedPhone);
// Both devices now have:
// title: 'Q3 Planning Notes' (from laptop -- higher timestamp)
// body: 'Discuss Q3 targets\n- Revenue goals' (from phone -- higher timestamp)Collaborative Record Editing
Multiple users on a team edit shared records concurrently. The CRDT strategy ensures edits to different fields never overwrite each other.
Context: A shared project management board where one team member updates a task's status while another adds an assignee at the same time.
const alice = new SyncEngine({ clientId: 'alice', conflictStrategy: 'crdt' });
const bob = new SyncEngine({ clientId: 'bob', conflictStrategy: 'crdt' });
// Both start with the same task (synced earlier)
const task = { id: 'task-1', title: 'Deploy v2', status: 'in-progress', assignee: 'alice' };
await alice.getBackend().set('tasks', { ...task, _version: 1, _timestamp: 1000, _deleted: false, _clientId: 'seed' });
await bob.getBackend().set('tasks', { ...task, _version: 1, _timestamp: 1000, _deleted: false, _clientId: 'seed' });
// Alice marks the task done
await alice.put('tasks', { id: 'task-1', title: 'Deploy v2', status: 'done', assignee: 'alice' });
// Bob reassigns it (concurrently, before seeing Alice's change)
await bob.put('tasks', { id: 'task-1', title: 'Deploy v2', status: 'in-progress', assignee: 'bob' });
// Sync Bob's changes into Alice
const bobRecords = await bob.getAll('tasks');
const result = await alice.sync('tasks', bobRecords);
// CRDT merge result:
// status: 'done' or 'in-progress' (whichever had the later timestamp wins)
// assignee: 'bob' (Bob's timestamp is later for this field)
// Both edits are tracked in result.conflicts[0].fieldsConflicted
console.log(`Conflicts resolved: ${result.conflicts.length}`);Edge / IoT Data Collection
Devices at the edge collect data locally and batch-sync to a central store on a schedule.
Context: Temperature sensors in a warehouse write readings to the local filesystem every second. An hourly cron job syncs the accumulated data to S3 for long-term storage.
// On the edge device -- collect readings locally
const sensor = new SyncEngine({
clientId: 'sensor-warehouse-3',
backend: 'filesystem',
filesystemConfig: { directory: '/var/sensor-data' },
});
// Runs every second
async function recordReading(temperature: number) {
await sensor.put('readings', {
id: `r-${Date.now()}`,
temperature,
location: 'warehouse-3',
});
}
// Hourly cron job: push to the central S3 store
async function syncToCloud(centralEngine: SyncEngine) {
const changes = await sensor.getChangesSince('readings', lastSyncTimestamp);
if (changes.length > 0) {
await centralEngine.sync('readings', changes);
lastSyncTimestamp = Date.now();
console.log(`Synced ${changes.length} readings to cloud`);
}
}Microservice Replication
Backend services in different regions each maintain a local cache and periodically reconcile with a shared data store.
Context: A product catalog API runs in US-East and EU-West. Each region has a Redis cache for low-latency reads. Changes are synced bidirectionally every 30 seconds so both regions converge.
// US-East service
const usEast = new SyncEngine({
clientId: 'us-east-1',
backend: 'redis',
redisConfig: { host: 'redis-us-east.internal', port: 6379 },
autoSync: true,
autoSyncInterval: 30_000,
});
// EU-West service
const euWest = new SyncEngine({
clientId: 'eu-west-1',
backend: 'redis',
redisConfig: { host: 'redis-eu-west.internal', port: 6379 },
autoSync: true,
autoSyncInterval: 30_000,
});
// Each region's auto-sync handler fetches changes from the other region
usEast.events.on('sync:start', async ({ collection }) => {
const remoteChanges = await fetchChangesFromRegion('eu-west', collection, lastSyncTimestamp);
if (remoteChanges.length > 0) {
await usEast.sync(collection, remoteChanges);
}
});Prototyping Distributed Sync
Test sync logic between services with zero infrastructure using the in-memory backend before committing to Redis or S3.
Context: You are designing a sync protocol for a new app and want to validate conflict resolution behavior in unit tests before deploying anything.
import { SyncEngine } from '@sakib11/syncforge';
// No infrastructure needed -- pure in-memory
const server = new SyncEngine({ clientId: 'server' });
const client1 = new SyncEngine({ clientId: 'client-1', conflictStrategy: 'crdt' });
const client2 = new SyncEngine({ clientId: 'client-2', conflictStrategy: 'crdt' });
// Simulate concurrent writes
await client1.put('docs', { id: 'd1', title: 'Draft', author: 'Alice' });
await client2.put('docs', { id: 'd1', title: 'Final', author: 'Bob' });
// Test the merge
const c1Records = await client1.getAll('docs');
const result = await client2.sync('docs', c1Records);
console.log('Conflicts:', result.conflicts.length);
console.log('Resolved:', await client2.get('docs', 'd1'));
// Swap backend to Redis when ready for production -- same API, same logic
// const prod = new SyncEngine({ backend: 'redis', redisConfig: { ... } });When NOT to Use
| Scenario | Why | Use Instead |
|----------|-----|-------------|
| Real-time character-level collaboration (Google Docs-style) | This library operates at the record/field level, not individual character positions or text ranges | Yjs, Automerge |
| High-throughput transactional workloads | No query language, no indexes, no ACID transactions. Not designed for thousands of writes per second | PostgreSQL, MySQL, DynamoDB |
| Strong consistency requirements | The engine is eventually consistent by design. There is no guarantee that a read immediately reflects a remote write | A centralized database with synchronous replication |
| Large binary files (images, videos, archives) | Records are serialized as JSON. Storing and diffing multi-megabyte blobs is inefficient | Object storage (S3, GCS) with a separate metadata sync |
| Single-writer systems | If only one process ever writes data, there are no conflicts to resolve. A regular database is simpler and faster | Any standard database or key-value store |
| Complex relational queries | No joins, no aggregations, no filtering beyond getAll and getChangesSince | SQLite, PostgreSQL, or an ORM layer |
Installation
npm install @sakib11/syncforgeFor Redis backend support:
npm install ioredisFor S3 backend support:
npm install @aws-sdk/client-s3Quick Start
import { SyncEngine } from '@sakib11/syncforge';
const engine = new SyncEngine();
// Store a record
await engine.put('users', { id: '1', name: 'Alice', age: 30 });
// Retrieve it
const user = await engine.get('users', '1');
console.log(user);
// { id: '1', name: 'Alice', age: 30, _version: 1, _timestamp: ..., _deleted: false, _clientId: '...' }
// Update it
await engine.put('users', { id: '1', name: 'Alice', age: 31 });
// Delete it (soft-delete)
await engine.delete('users', '1');
// Clean up
await engine.destroy();Configuration
Pass any subset of options to the constructor. All fields have sensible defaults.
import { SyncEngine } from '@sakib11/syncforge';
const engine = new SyncEngine({
backend: 'memory', // 'memory' | 'redis' | 's3' | 'filesystem'
conflictStrategy: 'timestamp', // 'timestamp' | 'crdt'
autoSync: false, // enable periodic auto-sync
autoSyncInterval: 30_000, // auto-sync interval in ms (min 1000)
maxRetries: 3, // retry attempts for failed syncs
batchSize: 100, // records per batch during sync
clientId: 'my-client', // unique client id (auto-generated if omitted)
});Full Options Reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| backend | 'memory' \| 'redis' \| 's3' \| 'filesystem' | 'memory' | Storage backend |
| conflictStrategy | 'timestamp' \| 'crdt' | 'timestamp' | Conflict resolution strategy |
| autoSync | boolean | false | Enable periodic sync timer |
| autoSyncInterval | number | 30000 | Timer interval in milliseconds |
| maxRetries | number | 3 | Max retry attempts on sync failure |
| batchSize | number | 100 | Records processed per batch |
| clientId | string | auto-generated | Unique identifier for this client |
| redisConfig | RedisConfig | -- | Required when backend is 'redis' |
| s3Config | S3Config | -- | Required when backend is 's3' |
| filesystemConfig | FilesystemConfig | -- | Required when backend is 'filesystem' |
Storage Backends
Memory (default)
No configuration needed. Data lives in-process and does not survive restarts.
const engine = new SyncEngine({ backend: 'memory' });Filesystem
Persists each collection as a directory and each record as a JSON file.
const engine = new SyncEngine({
backend: 'filesystem',
filesystemConfig: {
directory: './data',
},
});Redis
Stores records in Redis hashes with sorted-set timestamp indexes for efficient incremental queries. Requires the ioredis peer dependency.
const engine = new SyncEngine({
backend: 'redis',
redisConfig: {
host: 'localhost',
port: 6379,
password: 'secret', // optional
db: 0, // optional, default 0
keyPrefix: 'myapp', // optional, default 'dse'
tls: false, // optional
},
});Amazon S3
Stores records as individual JSON objects in S3. Requires the @aws-sdk/client-s3 peer dependency.
const engine = new SyncEngine({
backend: 's3',
s3Config: {
bucket: 'my-sync-bucket',
region: 'us-east-1',
prefix: 'sync-data', // optional, default 'dse'
accessKeyId: 'AKIA...', // optional (uses default credential chain if omitted)
secretAccessKey: '...', // optional
endpoint: 'http://localhost:9000', // optional, for MinIO/LocalStack
},
});CRUD Operations
put(collection, record)
Insert or update a record. The engine automatically manages _version, _timestamp, _deleted, and _clientId metadata.
const stored = await engine.put('users', { id: '1', name: 'Alice', age: 30 });
// stored._version === 1 (increments on each update)
// stored._clientId === engine.config.clientIdget(collection, id)
Retrieve a single record. Returns null if not found.
const user = await engine.get('users', '1');getAll(collection)
Retrieve all records in a collection.
const users = await engine.getAll('users');delete(collection, id)
Soft-delete a record by setting _deleted: true. The record remains in storage so the deletion propagates during sync.
const deleted = await engine.delete('users', '1');
// deleted._deleted === true
// deleted._version === previous._version + 1getChangesSince(collection, since)
Retrieve all records with a _timestamp greater than since. Used for incremental sync.
const lastSync = Date.now();
// ... time passes, records change ...
const changes = await engine.getChangesSince('users', lastSync);Synchronization
Basic Sync
The sync() method takes a collection name and an array of remote records, then:
- Computes deltas (added / updated / deleted / unchanged)
- Resolves conflicts using the configured strategy
- Persists results in batches
- Emits events throughout
const result = await engine.sync('users', remoteRecords);
console.log(result);
// {
// collection: 'users',
// added: 3,
// updated: 1,
// deleted: 0,
// conflicts: [...],
// timestamp: 1709654321000,
// duration: 12,
// }Two-Client Sync Example
import { SyncEngine } from '@sakib11/syncforge';
// Client A
const clientA = new SyncEngine({ clientId: 'client-a' });
await clientA.put('tasks', { id: 't1', title: 'Buy groceries', done: false });
await clientA.put('tasks', { id: 't2', title: 'Walk the dog', done: false });
// Client B starts empty
const clientB = new SyncEngine({ clientId: 'client-b' });
// Sync A → B
const aRecords = await clientA.getAll('tasks');
await clientB.sync('tasks', aRecords);
// Client B now has both tasks
// Client B marks a task done
await clientB.put('tasks', { id: 't1', title: 'Buy groceries', done: true });
// Sync B → A
const bRecords = await clientB.getAll('tasks');
await clientA.sync('tasks', bRecords);
// Client A now sees t1 as done
await clientA.destroy();
await clientB.destroy();Incremental Sync
Only transfer records that changed since the last checkpoint.
let lastSyncTimestamp = 0;
async function incrementalSync(local: SyncEngine, remoteEngine: SyncEngine) {
// Get only what changed on the remote side
const changes = await remoteEngine.getChangesSince('tasks', lastSyncTimestamp);
if (changes.length > 0) {
const result = await local.sync('tasks', changes);
console.log(`Synced: +${result.added} ~${result.updated} -${result.deleted}`);
}
lastSyncTimestamp = Date.now();
}Conflict Resolution
When two clients edit the same record, the engine detects a conflict and resolves it automatically.
Timestamp Strategy (default)
The record with the higher _timestamp wins entirely. Equal timestamps are broken by lexicographic _clientId comparison.
const engine = new SyncEngine({ conflictStrategy: 'timestamp' });Behavior: Client B edits a record at t=2000, Client A edits it at t=1000. After sync, B's version wins because 2000 > 1000.
CRDT Strategy (field-level merge)
Uses a Last-Writer-Wins Map so that concurrent updates to different fields are both preserved. Only fields where both clients wrote different values are true conflicts (resolved by higher timestamp).
const engine = new SyncEngine({ conflictStrategy: 'crdt' });Example:
// Client A updates age at t=1000
// { id: '1', name: 'Alice', age: 31, email: '[email protected]' }
// Client B updates email at t=2000
// { id: '1', name: 'Alice', age: 30, email: '[email protected]' }
// After CRDT merge:
// { id: '1', name: 'Alice', age: 31, email: '[email protected]' }
// age from A (no conflict — only A touched it... but if both touched it, higher timestamp wins)
// email from B (no conflict — only B touched it)Inspecting Conflicts
Every SyncResult includes a conflicts array with full details:
const result = await engine.sync('users', remoteRecords);
for (const conflict of result.conflicts) {
console.log(`Conflict on record ${conflict.recordId} in ${conflict.collection}`);
console.log(` Strategy: ${conflict.strategy}`);
console.log(` Local:`, conflict.localRecord);
console.log(` Remote:`, conflict.remoteRecord);
console.log(` Resolved:`, conflict.resolvedRecord);
if (conflict.fieldsConflicted) {
console.log(` Conflicted fields:`, conflict.fieldsConflicted);
}
}Events
The engine emits typed events for every lifecycle stage. Subscribe via engine.events.
engine.events.on('sync:start', (data) => {
console.log(`Sync started for ${data.collection} (${data.recordCount} records)`);
});
engine.events.on('sync:success', (result) => {
console.log(`Sync completed in ${result.duration}ms`);
});
engine.events.on('sync:error', (data) => {
console.error(`Sync failed for ${data.collection}:`, data.error.message);
});
engine.events.on('conflict', (detail) => {
console.warn(`Conflict on ${detail.recordId}, resolved via ${detail.strategy}`);
});
engine.events.on('record:added', ({ collection, record }) => {
console.log(`[${collection}] Added: ${record.id}`);
});
engine.events.on('record:updated', ({ collection, record, previous }) => {
console.log(`[${collection}] Updated: ${record.id} (v${previous._version} → v${record._version})`);
});
engine.events.on('record:deleted', ({ collection, record }) => {
console.log(`[${collection}] Deleted: ${record.id}`);
});Event Reference
| Event | Payload | Fired when |
|-------|---------|------------|
| sync:start | { collection, recordCount, timestamp } | Sync operation begins |
| sync:success | SyncResult | Sync completes successfully |
| sync:error | { collection, error, timestamp } | Sync fails after all retries |
| conflict | ConflictDetail | A conflict is detected and resolved |
| record:added | { collection, record } | A new record is stored |
| record:updated | { collection, record, previous } | An existing record is updated |
| record:deleted | { collection, record } | A record is soft-deleted |
One-Time Listeners
engine.events.once('sync:success', (result) => {
console.log('First sync completed!');
});Removing Listeners
const handler = (data) => console.log(data);
engine.events.on('sync:success', handler);
engine.events.off('sync:success', handler);
// Or remove all listeners for an event
engine.events.removeAllListeners('sync:success');
// Or remove all listeners entirely
engine.events.removeAllListeners();Auto-Sync
Enable a periodic timer that signals sync windows. Listen to sync:start events to provide remote records.
const engine = new SyncEngine({
autoSync: true,
autoSyncInterval: 10_000, // every 10 seconds
});
engine.events.on('sync:start', async ({ collection }) => {
// Fetch remote records from your server / peer
const remoteRecords = await fetchFromServer(collection);
if (remoteRecords.length > 0) {
await engine.sync(collection, remoteRecords);
}
});
// Stop the timer
engine.stopAutoSync();
// Restart it
engine.startAutoSync();Retry Logic
Failed sync() calls automatically retry with exponential back-off (100ms, 200ms, 400ms, ...). After all retries are exhausted, a sync:error event is emitted and the error is thrown.
const engine = new SyncEngine({ maxRetries: 5 });
engine.events.on('sync:error', ({ collection, error }) => {
console.error(`All retries exhausted for ${collection}:`, error.message);
});
try {
await engine.sync('users', remoteRecords);
} catch (err) {
// Thrown after 5 retries fail
}Batch Processing
Large sync operations are automatically split into batches. Configure the batch size:
const engine = new SyncEngine({ batchSize: 50 });
// Syncing 10,000 records processes them in chunks of 50
await engine.sync('logs', tenThousandRecords);CRDT Primitives
The library exposes its CRDT building blocks for advanced use cases.
LWWRegister
A Last-Writer-Wins Register. Concurrent writes are resolved by timestamp; ties are broken by clientId.
import { LWWRegister } from '@sakib11/syncforge';
const reg = new LWWRegister('initial', 1000, 'client-a');
// Update with a higher timestamp
reg.set('updated', 2000, 'client-b'); // returns true
console.log(reg.value); // 'updated'
// Rejected — lower timestamp
reg.set('old', 500, 'client-c'); // returns false
console.log(reg.value); // 'updated'
// Merge with another register's state
reg.merge({ value: 'merged', timestamp: 3000, clientId: 'client-c' });
console.log(reg.value); // 'merged'
// Serialize / deserialize
const state = reg.getState();
const restored = LWWRegister.from(state);LWWMap
A Last-Writer-Wins Map where each key is backed by an LWWRegister. Enables field-level conflict resolution.
import { LWWMap } from '@sakib11/syncforge';
const map = new LWWMap();
map.set('name', 'Alice', 1000, 'client-a');
map.set('age', 30, 1000, 'client-a');
console.log(map.get('name')); // 'Alice'
console.log(map.toRecord()); // { name: 'Alice', age: 30 }
// Merge with another map's state
const other = new LWWMap();
other.set('name', 'Bob', 2000, 'client-b');
other.set('email', '[email protected]', 2000, 'client-b');
map.merge(other.getState());
console.log(map.get('name')); // 'Bob' (higher timestamp)
console.log(map.get('email')); // '[email protected]' (new field)
console.log(map.get('age')); // 30 (preserved from original)Merging StoredRecords with LWWMap
import { LWWMap } from '@sakib11/syncforge';
import type { StoredRecord } from '@sakib11/syncforge';
const local: StoredRecord = {
id: '1', name: 'Alice', age: 31,
_version: 2, _timestamp: 2000, _deleted: false, _clientId: 'a',
};
const remote: StoredRecord = {
id: '1', name: 'Alice', email: '[email protected]',
_version: 2, _timestamp: 2000, _deleted: false, _clientId: 'b',
};
const { merged, conflictedFields } = LWWMap.mergeRecords(local, remote);
// merged.age === 31 (only local had this change)
// merged.email === '[email protected]' (only remote had this change)
// conflictedFields === [] (no true conflicts)Low-Level Utilities
computeDelta(localMap, remoteRecords)
Classify remote records as added, updated, deleted, or unchanged relative to a local record map.
import { computeDelta } from '@sakib11/syncforge';
const localMap = new Map([['1', localRecord]]);
const delta = computeDelta(localMap, remoteRecords);
// delta.added, delta.updated, delta.deleted, delta.unchangedresolveConflict(collection, local, remote, strategy)
Resolve a single conflict between two records.
import { resolveConflict } from '@sakib11/syncforge';
const detail = resolveConflict('users', localRecord, remoteRecord, 'crdt');
console.log(detail.resolvedRecord);
console.log(detail.fieldsConflicted);createBackend(config)
Instantiate a storage backend from a resolved config object.
import { createBackend, resolveConfig } from '@sakib11/syncforge';
const config = resolveConfig({ backend: 'memory' });
const backend = createBackend(config);
await backend.set('users', record);
const all = await backend.getAll('users');
await backend.destroy();StorageBackend Interface
All backends implement this interface. You can create a custom backend by implementing it:
import type { StorageBackend, StoredRecord } from '@sakib11/syncforge';
class MyCustomBackend implements StorageBackend {
async get(collection: string, id: string): Promise<StoredRecord | null> { /* ... */ }
async getAll(collection: string): Promise<StoredRecord[]> { /* ... */ }
async set(collection: string, record: StoredRecord): Promise<void> { /* ... */ }
async setBatch(collection: string, records: StoredRecord[]): Promise<void> { /* ... */ }
async delete(collection: string, id: string): Promise<void> { /* ... */ }
async getChangesSince(collection: string, since: number): Promise<StoredRecord[]> { /* ... */ }
async listCollections(): Promise<string[]> { /* ... */ }
async clearCollection(collection: string): Promise<void> { /* ... */ }
async destroy(): Promise<void> { /* ... */ }
}Record Metadata
Every record stored by the engine has these internal fields:
| Field | Type | Description |
|-------|------|-------------|
| _version | number | Monotonically increasing version counter |
| _timestamp | number | Date.now() at the time of the write |
| _deleted | boolean | true if the record has been soft-deleted |
| _clientId | string | ID of the client that performed the write |
These fields are managed automatically by put() and delete(). You should not set them manually on records passed to those methods.
TypeScript
The library is written in TypeScript and ships with full type declarations.
import type {
SyncRecord,
StoredRecord,
SyncEngineConfig,
SyncEngineOptions,
SyncResult,
ConflictDetail,
DeltaResult,
StorageBackend,
SyncEventMap,
SyncEventName,
BackendType,
ConflictStrategy,
RedisConfig,
S3Config,
FilesystemConfig,
} from '@sakib11/syncforge';Lifecycle
const engine = new SyncEngine({ autoSync: true, autoSyncInterval: 5000 });
// ... use the engine ...
// Stop auto-sync without destroying
engine.stopAutoSync();
// Restart auto-sync
engine.startAutoSync();
// Full teardown: stops timers, destroys backend, removes all listeners
await engine.destroy();
// After destroy, all operations throw
await engine.put('users', { id: '1' }); // throws '[SyncForge] Engine has been destroyed.'Development
# Install dependencies
npm install
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Build
npm run buildLicense
MIT
