UNPKG

zeno-db

Version:

A lightweight, offline-first client-side database with automatic sync capabilities

619 lines (515 loc) 15.3 kB
# ZenoDB <div align="center"> <img src="https://i.ibb.co/67ktWMm0/zenodb.png" alt="ZenoDB Logo" width="300" style="margin-bottom: 10px;" /> </div> # Zeno - Offline-First Sync System A lightweight, offline-first synchronization system that enables seamless data synchronization between client-side IndexedDB and server-side PostgreSQL. Perfect for note-taking applications and other data-heavy web apps that need to work offline. > ⚠️ **BETA VERSION NOTICE** > > This project is currently in beta stage and under active development. While it's functional, you may encounter: > - Unexpected behaviors > - Breaking changes between versions > - Incomplete features > - Limited documentation > > **Use in production with caution.** We recommend thorough testing in development/staging environments before deploying to production. > > Please report any issues you encounter on our GitHub repository. ## Table of Contents - [Features](#features) - [Installation](#installation) - [Quick Start](#quick-start) - [How It Works](#how-it-works) - [Configuration](#configuration) - [Sync Status & Events](#sync-status--events) - [Best Practices](#best-practices) - [Schema Migrations & Conflict Resolution](#schema-migrations--conflict-resolution) - [Examples](#examples) - [Troubleshooting](#troubleshooting) - [License](#license) - [TODO List & Roadmap](#todo-list--roadmap) ## Features - 🔄 **Real-time Sync**: Instant synchronization between all connected clients - 📱 **Offline Support**: Continue working without internet connection - 🔌 **Auto-Reconnect**: Automatically reconnects when network is available - 💾 **Persistent Storage**: IndexedDB (client) + PostgreSQL (server) -**WebSocket Protocol**: Fast, real-time updates across clients - 🔒 **Safe Transactions**: Ensures data consistency - 📊 **Sync Status**: Real-time sync status indicators and offline mode support - 🗑️ **Soft Delete**: Recoverable deletions with automatic cleanup ## Installation ```bash # Install the package npm install zeno-db # Or with yarn yarn add zeno-db ``` ## Quick Start ### Server Setup 1. Install dependencies: ```bash npm install zeno-db ``` 2. Create a server.js file: ```javascript import { startSyncServer } from 'zeno-db/server'; startSyncServer({ port: 3000, pg: { connectionString: 'your-postgresql-connection-string', table: 'notes' // Your table name } }); ``` ### Client Setup 1. Initialize the database in your app: ```javascript import { ZenoDB } from 'zeno-db'; const db = new ZenoDB({ storage: { type: 'indexeddb', name: 'my-app-db' }, sync: { type: 'websocket', url: 'ws://localhost:3000' }, clientId: `client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, softDelete: { enabled: true, fieldName: 'deletedAt', permanentDeleteAfter: 30 // Delete after 30 days } }); await db.init(); ``` ### Usage Example (Notes App) ```javascript // Create a new note const noteId = `note-${Date.now()}`; await db.set(noteId, { id: noteId, text: 'Your note text', createdAt: new Date().toISOString() }); // Read a note const note = await db.get(noteId); // Delete a note await db.delete(noteId); // Subscribe to changes db.subscribe((key, value) => { console.log('Note changed:', key, value); }); ``` ### Batch Operations Example You can efficiently get or set multiple records at once using batch operations: ```javascript // Batch get const notes = await db.getMany(['note-1', 'note-2', 'note-3']); // Batch set await db.setMany([ { id: 'note-4', text: 'Fourth note', createdAt: new Date().toISOString() }, { id: 'note-5', text: 'Fifth note', createdAt: new Date().toISOString() } ]); ``` ## Examples ### React Example with Sync Status ```jsx import { useState, useEffect } from 'react'; import { ZenoDB } from 'zeno-db'; function NotesApp() { const [db, setDb] = useState(null); const [syncStatus, setSyncStatus] = useState({ isOnline: false, isSyncing: false, syncProgress: 0, pendingChangesCount: 0 }); useEffect(() => { const initDB = async () => { const zenoDB = new ZenoDB({ storage: { type: 'indexeddb', name: 'notes-db' }, sync: { type: 'websocket', url: 'ws://localhost:3000' }, softDelete: { enabled: true, fieldName: 'deletedAt', permanentDeleteAfter: 30 // Delete after 30 days } }); await zenoDB.init(); setDb(zenoDB); // Subscribe to sync events zenoDB.subscribe((event) => { setSyncStatus(event.status); }); }; initDB(); }, []); return ( <div> <div className={`status ${syncStatus.isOnline ? 'online' : 'offline'}`}> {syncStatus.isOnline ? 'Online' : 'Offline'} </div> {syncStatus.isSyncing && ( <div className="progress"> Syncing... {syncStatus.syncProgress}% </div> )} </div> ); } ``` ### Vue Example ```vue <template> <div> <div :class="['status', { online: syncStatus.isOnline }]"> {{ syncStatus.isOnline ? 'Online' : 'Offline' }} </div> <div v-if="syncStatus.isSyncing" class="progress"> Syncing... {{ syncStatus.syncProgress }}% </div> </div> </template> <script> import { ZenoDB } from 'zeno-db'; export default { data() { return { db: null, syncStatus: { isOnline: false, isSyncing: false, syncProgress: 0, pendingChangesCount: 0 } }; }, async mounted() { this.db = new ZenoDB({ storage: { type: 'indexeddb', name: 'notes-db' }, sync: { type: 'websocket', url: 'ws://localhost:3000' }, softDelete: { enabled: true, fieldName: 'deletedAt', permanentDeleteAfter: 30 // Delete after 30 days } }); await this.db.init(); this.db.subscribe((event) => { this.syncStatus = event.status; }); } }; </script> ``` ## How It Works ### Client Side - Uses IndexedDB for local storage - Queues changes while offline - Auto-syncs when connection restores - Real-time updates via WebSocket - Sync status indicators and event handling ### Server Side - PostgreSQL database for persistent storage - WebSocket server for real-time communication - Handles concurrent updates - Broadcasts changes to all connected clients ## Configuration ### Server Config ```typescript interface ServerConfig { port: number; pg: { connectionString: string; // PostgreSQL connection string table: string; // Table name for your data }; softDelete?: { enabled: boolean; // Enable soft delete functionality fieldName?: string; // Field to store deletion timestamp (default: 'deletedAt') permanentDeleteAfter?: number; // Days to keep soft-deleted items }; } ``` ### Client Config ```typescript interface ClientConfig { storage: { type: 'indexeddb'; name: string; // IndexedDB database name }; sync: { type: 'websocket'; url: string; // WebSocket server URL }; clientId: string; // Unique client identifier softDelete?: { enabled: boolean; // Enable soft delete functionality fieldName?: string; // Field to store deletion timestamp (default: 'deletedAt') permanentDeleteAfter?: number; // Days to keep soft-deleted items }; } ``` ### Usage Example with Soft Delete ```javascript // Initialize with soft delete enabled const db = new ZenoDB({ storage: { type: 'indexeddb', name: 'my-app-db' }, sync: { type: 'websocket', url: 'ws://localhost:3000' }, clientId: 'client-1', softDelete: { enabled: true, fieldName: 'deletedAt', permanentDeleteAfter: 30 // Delete after 30 days } }); // Soft delete an item await db.delete('note-1'); // Restore a soft-deleted item await db.restore('note-1'); // Get all items including soft-deleted ones const allItems = await db.getAll(true); // Permanently delete an item await db.delete('note-1', true); // Purge old soft-deleted items await db.purgeDeleted(); ``` ## Sync Status & Events ZenoDB provides real-time sync status updates and event handling to help you build responsive UIs that reflect the current sync state of your application. ```typescript import { ZenoDB, SyncEvent, SyncStatus } from 'zeno-db'; // Initialize your database const db = new ZenoDB({ storage: { type: 'indexeddb', name: 'my-app-db' }, sync: { type: 'websocket', url: 'ws://localhost:3000' }, softDelete: { enabled: true, fieldName: 'deletedAt', permanentDeleteAfter: 30 // Delete after 30 days } }); // Subscribe to sync events db.subscribe((event: SyncEvent) => { const { type, status } = event; // status contains: // - isOnline: boolean (connection status) // - isSyncing: boolean (whether sync is in progress) // - syncProgress: number (0-100 percentage) // - pendingChangesCount: number (changes waiting to sync) switch(type) { case 'connection_change': updateConnectionUI(status.isOnline); break; case 'sync_started': showSyncStarted(); break; case 'sync_progress': updateProgressBar(status.syncProgress); break; case 'sync_completed': showSyncComplete(); break; } }); // Get current status at any time const currentStatus: SyncStatus = db.getSyncStatus(); ``` #### Sync Event Types - `connection_change`: Fired when connection status changes (online/offline) - `sync_started`: Fired when sync operation begins - `sync_progress`: Fired during sync with progress updates - `sync_completed`: Fired when sync operation completes #### Sync Status Properties - `isOnline`: Current connection status - `isSyncing`: Whether a sync operation is in progress - `syncProgress`: Progress percentage (0-100) - `pendingChangesCount`: Number of changes waiting to sync ## Best Practices 1. Always initialize before use: ```javascript await db.init(); ``` 2. Handle offline/online transitions gracefully: ```javascript window.addEventListener('online', () => { console.log('Back online, syncing...'); }); ``` 3. Use try-catch for error handling: ```javascript try { await db.set('key', value); } catch (error) { console.error('Error saving data:', error); } ``` 4. Monitor sync status: ```javascript db.subscribe((event) => { const { type, status } = event; // Update UI based on sync status. }); ``` 5. Use soft delete for important data: ```javascript // Instead of permanent deletion await db.delete('important-data'); // Later, if needed await db.restore('important-data'); // Clean up old deleted items periodically await db.purgeDeleted(); ``` ## Schema Migrations & Conflict Resolution ### Client-side Migration Example When evolving your data schema, you may need to migrate existing records. For example, if you add a new field to your notes, you can write a migration function that updates all records after initializing the database: ```js // Example: Migrating notes to add a "tags" field if missing async function migrateNotes(db) { const allNotes = await db.getAll(); for (const note of allNotes) { if (!note.tags) { note.tags = []; await db.set(note.id, note); // Save migrated note } } } // Run migration after db.init() await db.init(); await migrateNotes(db); ``` *Tip: Store a schema version in each record or in IndexedDB metadata, and only run migrations when needed.* ### Application-level Merging Example For collaborative or complex data structures, you may need to merge changes from multiple sources. Here is a simple example for merging two versions of a collaborative list: ```js // Example: Merging two versions of a collaborative list function mergeLists(localList, remoteList) { const merged = [...localList]; for (const item of remoteList) { if (!merged.find(i => i.id === item.id)) { merged.push(item); // Add new items from remote } // Optionally, resolve conflicts for items with the same id } return merged; } // Usage in sync event handler // (Assuming your sync system emits a 'sync_conflict' event) db.subscribe((event) => { if (event.type === 'sync_conflict') { const merged = mergeLists(event.local, event.remote); db.set(event.key, merged); } }); ``` *Tip: For more complex data, consider using libraries like Automerge or Yjs for CRDT-based merging.* ## PostgreSQL Table Structure Your PostgreSQL table should have this structure: ```sql CREATE TABLE notes ( id TEXT PRIMARY KEY, data JSONB NOT NULL, deleted_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Index for soft delete queries CREATE INDEX idx_notes_deleted_at ON notes(deleted_at); ``` ## License MIT License - see LICENSE file for details. ## TODO List & Roadmap ### Authentication & Security 🔐 - [ ] User authentication system integration - [ ] JWT-based authentication - [ ] Role-based access control (RBAC) - [ ] API rate limiting - [ ] Data encryption at rest - [ ] Session management - [ ] OAuth2 provider integration (Google, GitHub) ### Data Management 📊 - [ ] Data versioning - [ ] Soft delete functionality - [ ] Data validation middleware - [ ] Advanced conflict resolution strategies - [ ] Bulk import/export functionality ### Performance Optimizations ⚡ - [ ] Data compression - [ ] Caching layer - [ ] Request batching - [ ] Lazy loading support - [ ] Connection pooling - [ ] Query optimization ### Developer Experience 🛠️ - [ ] CLI tool for database management - [ ] Better error handling and logging - [ ] Development environment tooling - [ ] TypeScript type definitions - [ ] API documentation with Swagger/OpenAPI - [ ] Integration tests - [ ] E2E testing suite ### Monitoring & Debugging 📈 - [ ] Telemetry integration - [ ] Performance metrics dashboard - [ ] Debug logging - [ ] Error tracking integration - [ ] Health check endpoints - [ ] Audit logging ### Infrastructure 🏗️ - [ ] Docker containerization - [ ] CI/CD pipeline setup - [ ] Automated deployment scripts - [ ] Database migration tools - [ ] Backup and restore functionality - [ ] High availability setup ## Troubleshooting ### Common Issues 1. **Connection Issues** ```javascript // Check if WebSocket server is running const ws = new WebSocket('ws://localhost:3000'); ws.onerror = (error) => { console.error('WebSocket connection error:', error); }; ``` 2. **Database Initialization Failures** ```javascript try { await db.init(); } catch (error) { console.error('Database initialization failed:', error); // Check if IndexedDB is supported if (!window.indexedDB) { console.error('IndexedDB is not supported in this browser'); } } ``` 3. **Sync Status Not Updating** - Ensure you're properly subscribing to sync events - Check network connectivity - Verify WebSocket connection is active ### Debugging Tips 1. Enable verbose logging: ```javascript const db = new ZenoDB({ // ... config debug: true }); ``` 2. Monitor sync events: ```javascript db.subscribe((event) => { console.log('Sync event:', event); }); ```