zeno-db
Version:
A lightweight, offline-first client-side database with automatic sync capabilities
619 lines (515 loc) • 15.3 kB
Markdown
# 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);
});
```