A reactive sync engine framework for use with Tonk apps.
npm install @tonk/keepsync
# or
yarn add @tonk/keepsync
# or
pnpm add @tonk/keepsync
- Real-time synchronization using CRDTs (Conflict-free Replicated Data Types)
- Built on Automerge for robust conflict resolution
- Seamless integration with React and Zustand
- Automatic reconnection and offline support
- Simple WebSocket-based synchronization
- Persistent storage for offline-first applications
- File system synchronization for collaborative file management
Initialize the sync engine in your application entry point (or before using any synced stores):
// index.tsx
import { initializeSyncEngine } from '@tonk/keepsync';
// Initialize the sync engine
initializeSyncEngine({
url: 'ws://localhost:4080',
name: 'MySyncEngine',
onSync: docId => console.log(`Document ${docId} synced`),
onError: error => console.error('Sync error:', error),
});
Use the sync
middleware to create stores that automatically synchronize with other clients:
// stores/counterStore.ts
import { create } from 'zustand';
import { sync } from '@tonk/keepsync';
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
export const useCounterStore = create<CounterState>(
sync(
// The store implementation
set => ({
count: 0,
// Increment the counter
increment: () => {
set(state => ({ count: state.count + 1 }));
},
// Decrement the counter
decrement: () => {
set(state => ({ count: Math.max(0, state.count - 1) }));
},
// Reset the counter
reset: () => {
set({ count: 0 });
},
}),
// Sync configuration
{
docId: 'counter',
// Optional: configure initialization timeout
initTimeout: 30000,
// Optional: handle initialization errors
onInitError: error => console.error('Sync initialization error:', error),
}
)
);
// components/Counter.tsx
import React from 'react';
import { useCounterStore } from '../stores/counterStore';
export function Counter() {
// Use the store hook directly - sync is handled by the middleware
const { count, increment, decrement, reset } = useCounterStore();
return (
<div>
<h2>Collaborative Counter: {count}</h2>
<div>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
<button onClick={reset}>Reset</button>
</div>
<p>
<small>
Open this app in multiple windows to see real-time collaboration in action.
</small>
</p>
</div>
);
}
If you need to access the sync engine directly:
import { getSyncInstance } from '@tonk/keepsync';
function syncDocument() {
const syncEngine = getSyncInstance();
if (syncEngine) {
// Manually update a document
syncEngine.updateDocument('my-document', doc => {
doc.someProperty = 'new value';
});
// Get a document
syncEngine.getDocument('my-document').then(doc => {
console.log('Document content:', doc);
});
} else {
console.warn('Sync engine not initialized yet');
}
}
KeepSync also provides a file system API for collaborative file management:
import {
configureSyncedFileSystem,
addFile,
getAllFiles,
getFile,
removeFile,
} from '@tonk/keepsync';
// Configure the synced file system
configureSyncedFileSystem({
docId: 'my-files',
dbName: 'my-app-files',
storeName: 'file-blobs',
});
// Add a file
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
fileInput.addEventListener('change', async () => {
if (fileInput.files && fileInput.files.length > 0) {
const file = fileInput.files[0];
const metadata = await addFile(file);
console.log('Added file:', metadata);
}
});
// List all files
async function displayAllFiles() {
const files = await getAllFiles();
console.log('All files:', files);
// Display files in UI
const fileList = document.getElementById('fileList');
if (fileList) {
fileList.innerHTML = '';
files.forEach(file => {
const item = document.createElement('div');
item.textContent = `${file.name} (${file.size} bytes)`;
// Add download button
const downloadBtn = document.createElement('button');
downloadBtn.textContent = 'Download';
downloadBtn.onclick = async () => {
const blob = await getFile(file.hash);
if (blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.name;
a.click();
URL.revokeObjectURL(url);
}
};
// Add delete button
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = async () => {
await removeFile(file.hash);
displayAllFiles(); // Refresh the list
};
item.appendChild(downloadBtn);
item.appendChild(deleteBtn);
fileList.appendChild(item);
});
}
}
KeepSync uses a simple WebSocket server for synchronization:
import { WebSocketServer } from 'ws';
import { createServer } from 'http';
// Create a simple HTTP server
const server = createServer();
const wss = new WebSocketServer({
server,
path: '/sync',
});
// Store connected clients
const connections = new Set();
// Handle WebSocket connections
wss.on('connection', ws => {
console.log('Client connected');
connections.add(ws);
// Handle messages from clients
ws.on('message', data => {
connections.forEach(client => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data.toString());
}
});
});
// Handle client disconnection
ws.on('close', () => {
console.log('Client disconnected');
connections.delete(ws);
});
});
// Start the server
const PORT = process.env.PORT || 8040;
server.listen(PORT, () => {
console.log(`Sync server running on port ${PORT}`);
});
When your application is shutting down, make sure to clean up resources:
import { closeSyncEngine, closeSyncedFileSystem } from '@tonk/keepsync';
// Clean up everything when the app is shutting down
function shutdownApp() {
closeSyncedFileSystem();
closeSyncEngine();
}
MIT