Basic Usage
1. Set Up the Sync Provider
Initialize the sync engine in your application entry point (or before using any synced stores):
// index.tsx
import { configureSyncEngine } from '@tonk/keepsync';
import { BrowserWebSocketClientAdapter } from '@automerge/automerge-repo-network-websocket';
import { IndexedDBStorageAdapter } from '@automerge/automerge-repo-storage-indexeddb';
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${window.location.host}/sync`;
const wsAdapter = new BrowserWebSocketClientAdapter(wsUrl);
const storage = new IndexedDBStorageAdapter();
const url =
window.location.host.indexOf('localhost') === 0
? 'http://localhost:7777'
: `${window.location.protocol}//${window.location.host}`;
await configureSyncEngine({
url,
network: [wsAdapter as any],
storage,
});
2. Create a Synced Store with the Middleware
Use the sync
middleware to create stores that automatically synchronize with other clients:
// stores/counterStore.ts
import { create } from 'zustand';
import { sync, DocumentId } 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' as DocumentId,
// Optional: configure initialization timeout
initTimeout: 30000,
// Optional: handle initialization errors
onInitError: error => console.error('Sync initialization error:', error),
}
)
);
3. Use the Store in React Components
// 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>
);
}
Directly reading and writing documents
You can also directly read and write documents and address them using paths similar to a filesystem. This is useful for when you need more fine-grained control over document access and a zustand store is too cumbersome (e.g. when you want each document to have its own space and be directly addressable);
import { readDoc, writeDoc, ls, mkDir, rm, listenToDoc } from "@tonk/keepsync";
/**
* Reads a document from keepsync
*
* This function retrieves a document at the specified path in your sync engine.
* It returns the document content if found, or undefined if the document doesn't exist.
*
* @param path - The path identifying the document to read
* @returns Promise resolving to the document content or undefined if not found
* @throws Error if the SyncEngine is not properly initialized
*/
readDoc = async <T>(path: string): Promise<T | undefined>;
/**
* Writes content to a document to keepsync
*
* This function creates or updates a document at the specified path.
* If the document doesn't exist, it creates a new one.
* If the document already exists, it updates it with the provided content.
*
* @param path - The path identifying the document to write
* @param content - The content to write to the document
* @throws Error if the SyncEngine is not properly initialized
*/
writeDoc = async <T>(path: string, content: T);
/**
* Lists documents at a specified path
*
* This function retrieves a list of documents at the specified directory path.
* It returns an array of document names found at that path.
*
* @param path - The directory path to list documents from
* @returns Promise resolving to an array of document names
* @throws Error if the SyncEngine is not properly initialized
*/
ls = async (path: string): Promise<string[]>;
/**
* Creates a directory at the specified path
*
* This function creates a new directory at the specified path.
* If the directory already exists, it does nothing.
*
* @param path - The path where the directory should be created
* @throws Error if the SyncEngine is not properly initialized
*/
mkDir = async (path: string): Promise<void>;
/**
* Removes a document or directory at the specified path
*
* This function deletes a document or directory at the specified path.
* If removing a directory, it will remove all documents within it.
*
* @param path - The path of the document or directory to remove
* @param recursive - Whether to recursively remove directories (default: false)
* @throws Error if the SyncEngine is not properly initialized
*/
rm = async (path: string, recursive?: boolean): Promise<void>;
/**
* Listens for changes to a document
*
* This function sets up a listener for changes to a document at the specified path.
* The callback will be called whenever the document changes with detailed patch information.
*
* @param path - The path of the document to listen to
* @param callback - Function to call when the document changes, receives payload with doc, patches, patchInfo, and handle
* @returns A function that can be called to stop listening
* @throws Error if the SyncEngine is not properly initialized
*/
listenToDoc = <T>(path: string, callback: (payload: { doc: T; patches: any[]; patchInfo: any; handle: DocHandle<T> }) => void): Promise<() => void>;
File System Operations Example
Here's an example of how to use the file system operations:
import { ls, mkDir, rm, readDoc, writeDoc, listenToDoc } from '@tonk/keepsync';
// Create a directory structure
await mkDir('/users');
// Write a document
await writeDoc('/users/user1', { name: 'Alice', age: 30 });
await writeDoc('/users/user2', { name: 'Bob', age: 25 });
// List documents in a directory
const users = await ls('/users');
console.log(users); // ["user1", "user2"]
// Read a document
const user1 = await readDoc<{ name: string; age: number }>('/users/user1');
console.log(user1); // { name: "Alice", age: 30 }
// Listen for changes to a document
const unsubscribe = await listenToDoc<{ name: string; age: number }>('/users/user1', payload => {
const { doc: user, patches, patchInfo, handle } = payload;
if (user) {
console.log(`User updated: ${user.name}, ${user.age}`);
console.log('Patches:', patches);
console.log('Patch info:', patchInfo);
}
});
// Update the document (will trigger the listener)
await writeDoc('/users/user1', { name: 'Alice', age: 31 });
// Stop listening when done
unsubscribe();
// Remove a document
await rm('/users/user2');
// Remove a directory and all its contents
await rm('/users', true);