@synckit-js/sdk
v0.2.2
Published
Production-ready local-first sync with rich text editing, undo/redo, live cursors, and framework adapters for React, Vue, and Svelte
Maintainers
Readme
@synckit-js/sdk
TypeScript SDK for SyncKit - Production-grade local-first sync with real-time collaboration.
Bundle Size: 59KB gzipped (full) or 45KB gzipped (lite) - Competitive with Yjs (~19KB), Automerge (~60-78KB), and Firebase (~150KB).
🚀 Quick Start
Offline-Only Mode
import { SyncKit } from '@synckit-js/sdk'
// Initialize (offline-only)
const sync = new SyncKit({
storage: 'indexeddb',
name: 'my-app'
})
await sync.init()
// Create a typed document
interface Todo {
title: string
completed: boolean
}
const doc = sync.document<Todo>('todo-1')
// Initialize document
await doc.init()
// Set fields
await doc.set('title', 'Buy milk')
await doc.set('completed', false)
// Subscribe to changes
doc.subscribe((todo) => {
console.log('Updated:', todo)
})
// Get current state
const todo = doc.get()With Network Sync (v0.1.0)
import { SyncKit } from '@synckit-js/sdk'
// Initialize with server sync
const sync = new SyncKit({
storage: 'indexeddb',
name: 'my-app',
serverUrl: 'ws://localhost:8080', // Enable network sync
clientId: 'user-123',
network: {
reconnect: {
enabled: true,
initialDelay: 1000,
maxDelay: 30000
}
}
})
await sync.init()
// Monitor network status
sync.onNetworkStatusChange((status) => {
console.log('Connection:', status.connectionState)
console.log('Queue size:', status.queueSize)
})
// Create and sync document
const doc = sync.document<Todo>('todo-1')
await doc.init() // Automatically subscribes to real-time server updates!
await doc.update({ title: 'Buy milk', completed: false })
// Changes sync instantly to server and other clients📦 Installation
npm install @synckit-js/sdk
# or
yarn add @synckit-js/sdk
# or
pnpm add @synckit-js/sdk🎯 Features
Core Features
- ✅ Type-safe: Full TypeScript support with generics
- ✅ Reactive: Observable pattern for real-time updates
- ✅ Persistent: IndexedDB storage with unlimited capacity
- ✅ Offline-first: Works completely without network
- ✅ Zero-config: Sensible defaults, no setup required
Network Features (v0.1.0)
- ✅ Real-time sync: WebSocket-based server synchronization
- ✅ Conflict resolution: Automatic LWW with vector clocks
- ✅ Offline queue: Persistent operation queue with retry logic
- ✅ Auto-reconnection: Exponential backoff with jitter
- ✅ Network monitoring: Connection state tracking
- ✅ Sync state tracking: Per-document sync status
Framework Integration
- ✅ React hooks: Built-in hooks for React 18+
- ✅ Vue composables: Full Vue 3 Composition API support
- ✅ Svelte stores: Hybrid Svelte 4/5 store implementation
- ✅ Network-aware hooks: Monitor connection and sync state
- ✅ TypeScript support: Full type inference throughout
Undo/Redo (v0.2.0)
- ✅ Intelligent merging: Automatically merges consecutive operations
- ✅ Cross-tab sync: Undo/redo state syncs across browser tabs
- ✅ Persistent history: Survives page refreshes via IndexedDB
- ✅ Framework adapters: Native React/Vue/Svelte integrations
- ✅ Customizable: Configure merge strategies and stack size
🔌 React Integration
Basic Usage
import { SyncProvider, useSyncDocument } from '@synckit-js/sdk/react'
// 1. Wrap your app
function App() {
return (
<SyncProvider synckit={sync}>
<TodoList />
</SyncProvider>
)
}
// 2. Use in components
function TodoItem({ id }: { id: string }) {
const [todo, { set, update, delete: deleteFn }, doc] = useSyncDocument<Todo>(id)
return (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={(e) => set('completed', e.target.checked)}
/>
<span>{todo.title}</span>
<button onClick={() => update({ completed: !todo.completed })}>
Toggle
</button>
</div>
)
}Network-Aware Components (v0.1.0)
import { useNetworkStatus, useSyncState } from '@synckit-js/sdk/react'
function NetworkIndicator() {
const status = useNetworkStatus()
if (!status) return null // Offline-only mode
return (
<div>
<span>Status: {status.connectionState}</span>
<span>Queue: {status.queueSize} operations</span>
<span>{status.isOnline ? '🟢 Online' : '🔴 Offline'}</span>
</div>
)
}
function DocumentSyncStatus({ docId }: { docId: string }) {
const syncState = useSyncState(docId)
if (!syncState) return null
return (
<div>
{syncState.isSynced ? '✅ Synced' : '⏳ Syncing...'}
<span>Last sync: {new Date(syncState.lastSyncedAt).toLocaleString()}</span>
</div>
)
}↩️ Undo/Redo
SyncKit includes a powerful undo/redo system with cross-tab synchronization and intelligent operation merging.
React
import { useUndo } from '@synckit-js/sdk/react'
function TextEditor() {
const [text, setText] = useState('')
const { canUndo, canRedo, undo, redo, add } = useUndo('doc-123')
const handleChange = (newText: string) => {
const oldText = text
setText(newText)
add({
type: 'text-change',
data: { from: oldText, to: newText }
})
}
const handleUndo = () => {
const op = undo()
if (op?.data) setText(op.data.from)
}
const handleRedo = () => {
const op = redo()
if (op?.data) setText(op.data.to)
}
return (
<div>
<button onClick={handleUndo} disabled={!canUndo}>Undo</button>
<button onClick={handleRedo} disabled={!canRedo}>Redo</button>
<textarea value={text} onChange={e => handleChange(e.target.value)} />
</div>
)
}Vue
<script setup lang="ts">
import { ref } from 'vue'
import { useUndo } from '@synckit-js/sdk/vue'
const text = ref('')
const { canUndo, canRedo, undo, redo, add } = useUndo('doc-123')
const handleChange = (newText: string) => {
const oldText = text.value
text.value = newText
add({
type: 'text-change',
data: { from: oldText, to: newText }
})
}
const handleUndo = () => {
const op = undo()
if (op?.data) text.value = op.data.from
}
const handleRedo = () => {
const op = redo()
if (op?.data) text.value = op.data.to
}
</script>
<template>
<div>
<button @click="handleUndo" :disabled="!canUndo">Undo</button>
<button @click="handleRedo" :disabled="!canRedo">Redo</button>
<textarea :value="text" @input="handleChange($event.target.value)" />
</div>
</template>Svelte
<script>
import { undo } from '@synckit-js/sdk/svelte'
let text = ''
const undoStore = undo('doc-123')
function handleChange(event) {
const newText = event.target.value
const oldText = text
text = newText
undoStore.add({
type: 'text-change',
data: { from: oldText, to: newText }
})
}
function handleUndo() {
const op = undoStore.undo()
if (op?.data) text = op.data.from
}
function handleRedo() {
const op = undoStore.redo()
if (op?.data) text = op.data.to
}
</script>
<button on:click={handleUndo} disabled={!$undoStore.canUndo}>Undo</button>
<button on:click={handleRedo} disabled={!$undoStore.canRedo}>Redo</button>
<textarea value={text} on:input={handleChange} />Key Features
- Intelligent Merging: Consecutive operations automatically merge (e.g., typing becomes one undo unit)
- Cross-Tab Sync: Undo/redo state syncs across browser tabs in real-time
- Persistent: History survives page refreshes via IndexedDB
- Customizable: Configure merge windows, stack size, and custom merge strategies
- Keyboard Shortcuts: Built-in support for Ctrl+Z and Ctrl+Y
See UNDO_REDO.md for complete API documentation.
📚 API Reference
SyncKit
Constructor:
new SyncKit(config?: SyncKitConfig)
interface SyncKitConfig {
storage?: 'indexeddb' | 'memory' | StorageAdapter
name?: string
serverUrl?: string // Enable network sync
clientId?: string // Client identifier
network?: NetworkConfig // Network options
}Core Methods:
init()- Initialize the SDKdocument<T>(id)- Get or create a documentlistDocuments()- List all document IDsdeleteDocument(id)- Delete a documentclearAll()- Clear all documentsgetClientId()- Get client identifierisInitialized()- Check initialization status
Network Methods (v0.1.0):
getNetworkStatus()- Get current network statusgetSyncState(documentId)- Get document sync stateonNetworkStatusChange(callback)- Subscribe to network changesonSyncStateChange(documentId, callback)- Subscribe to sync statesyncDocument(documentId)- Manually trigger sync
SyncDocument
Methods:
init()- Initialize document (required before use)get()- Get current state (synchronous)getField(field)- Get a single fieldset(field, value)- Set a field (async)update(updates)- Update multiple fields (async)delete(field)- Delete a field (async)subscribe(callback)- Subscribe to changesunsubscribe(callback)- Unsubscribe from changestoJSON()- Export as JSONmerge(other)- Merge with another document
Important: Always call await doc.init() before using a document. When a serverUrl is configured, init() automatically subscribes the document to real-time server updates, enabling instant synchronization with other clients.
React Hooks
Core Hooks:
useSyncKit()- Get SyncKit instance from contextuseSyncDocument<T>(id)- Sync a document (returns[data, actions, document])useSyncField<T, K>(id, field)- Sync a single fielduseSyncDocumentList()- List all document IDs
Network Hooks (v0.1.0):
useNetworkStatus()- Monitor connection statususeSyncState(documentId)- Monitor document sync stateuseSyncDocumentWithState<T>(id)- Document + sync state combined
📊 Bundle Size
Production Bundles (gzipped)
| Build | Total Size | JavaScript | WASM | Use Case | |-------|------------|------------|------|----------| | Full SDK | 59KB | 10KB | 49KB | Complete with network sync | | Lite SDK | 45KB | 1.5KB | 44KB | Offline-only, no network |
Network overhead: Only 14KB gzipped for complete WebSocket + sync implementation.
Uncompressed Sizes
| Build | Total | JavaScript | WASM | |-------|-------|------------|------| | Full (ESM) | 138KB | 45KB | 93KB | | Full (CJS) | 156KB | 63KB | 93KB | | Lite (ESM) | 85KB | 5.1KB | 80KB | | Lite (CJS) | 102KB | 22KB | 80KB |
Comparison
| Library | Size (gzipped) | Offline-First | Real-time Sync | |---------|----------------|---------------|----------------| | SyncKit Full | 59KB | ✅ Native | ✅ Built-in | | SyncKit Lite | 45KB | ✅ Native | ❌ No | | Yjs | ~19KB | ⚠️ Limited | ✅ Yes | | Automerge | ~60-78KB | ✅ Native | ✅ Yes | | Supabase | ~45KB | ❌ Cache only | ✅ Yes | | Firebase | ~150KB | ⚠️ Cache only | ✅ Yes |
Competitive and feature-complete - Best balance of size and functionality.
🔧 Storage Adapters
IndexedDB (Browser - Recommended)
const sync = new SyncKit({ storage: 'indexeddb' })Features:
- Unlimited storage capacity
- Persistent across sessions
- Async operations
- Works in all modern browsers
Memory (Testing/Development)
const sync = new SyncKit({ storage: 'memory' })Features:
- Fast in-memory storage
- No persistence
- Great for testing
- No browser APIs needed
Custom Adapter
import type { StorageAdapter } from '@synckit-js/sdk'
class MyStorage implements StorageAdapter {
async get(key: string): Promise<string | null> {
// Your implementation
}
async set(key: string, value: string): Promise<void> {
// Your implementation
}
async delete(key: string): Promise<void> {
// Your implementation
}
async clear(): Promise<void> {
// Your implementation
}
async keys(): Promise<string[]> {
// Your implementation
}
}
const sync = new SyncKit({ storage: new MyStorage() })🌐 Network Configuration
Basic Configuration
const sync = new SyncKit({
serverUrl: 'ws://localhost:8080',
clientId: 'user-123',
network: {
reconnect: {
enabled: true,
initialDelay: 1000, // 1 second
maxDelay: 30000, // 30 seconds
backoffMultiplier: 1.5,
maxAttempts: Infinity
},
heartbeat: {
interval: 30000, // 30 seconds
timeout: 5000 // 5 seconds
},
queue: {
maxSize: 1000, // Max queued operations
persistentStorage: true // Survive restarts
}
}
})Network Status
const status = sync.getNetworkStatus()
console.log(status.connectionState) // 'connected' | 'connecting' | 'disconnected' | 'reconnecting' | 'failed'
console.log(status.isOnline) // Network connectivity
console.log(status.queueSize) // Pending operations
console.log(status.lastConnectedAt) // Last successful connection
console.log(status.reconnectAttempts) // Failed connection attemptsSync State
const state = sync.getSyncState('doc-1')
console.log(state.isSynced) // All changes synced?
console.log(state.isSyncing) // Currently syncing?
console.log(state.hasError) // Sync error occurred?
console.log(state.lastSyncedAt) // Last successful sync
console.log(state.pendingOps) // Operations waiting to sync🧪 Development Status
v0.1.0 - Current Release ✅
Core Infrastructure:
- ✅ Document API with TypeScript generics
- ✅ Storage adapters (IndexedDB, Memory)
- ✅ React hooks integration
- ✅ LWW conflict resolution with vector clocks
Network Layer (NEW in v0.1.0):
- ✅ WebSocket client with auto-reconnection
- ✅ Binary message protocol
- ✅ Offline queue with persistent storage
- ✅ Sync manager with conflict resolution
- ✅ Network state tracking
- ✅ React network hooks
Test Coverage:
- ✅ 100% test pass rate (100/100 tests)
- ✅ Unit tests: 100% passing
- ✅ Integration tests: 100% passing
- ✅ Performance benchmarks included
v0.2.0 - Planned
Advanced CRDTs:
- 🚧 Text CRDTs for character-level editing
- 🚧 Counters for distributed counting
- 🚧 Sets for unique collections
- 🚧 Maps for nested structures
Enhanced Network:
- 🚧 End-to-end encryption
- 🚧 Compression for large payloads
- 🚧 Presence indicators (who's online)
- 🚧 Advanced conflict resolution strategies
📝 Examples
Complete working examples available:
- Collaborative Editor - Markdown/code editor with real-time collaboration
- Project Management - Kanban board with drag-and-drop
- Todo App - Simple todo list with sync
🚀 Performance
Benchmarks (v0.1.0)
| Operation | Performance | Notes | |-----------|-------------|-------| | Single field update | ~371ns | <1ms consistently | | Document merge | ~74µs | Extremely fast | | Message encoding | 5.05ms/1000 | 0.005ms per message | | Message decoding | 19.62ms/1000 | 0.020ms per message | | Queue operations | 21.21ms/1000 | 47K ops/sec | | Vector clock merge | 0.30ms/100 | Conflict resolution |
See PERFORMANCE.md for detailed benchmarks.
🔒 Type Safety
Full TypeScript support with strict type inference:
interface User {
name: string
email: string
age: number
}
const doc = sync.document<User>('user-1')
await doc.init()
// ✅ Type-safe field access
await doc.set('name', 'Alice') // Valid
await doc.set('age', 25) // Valid
// ❌ TypeScript errors
await doc.set('name', 123) // Error: Type 'number' not assignable to 'string'
await doc.set('invalid', 'value') // Error: 'invalid' not in type 'User'
// ✅ Type-safe updates
await doc.update({
name: 'Bob',
age: 30
})
// ❌ TypeScript error
await doc.update({
invalid: 'field' // Error: Object literal may only specify known properties
})🤝 Contributing
See CONTRIBUTING.md for development guidelines.
📄 License
MIT - see LICENSE for details.
