5.9 KiB
Enterprise Storage Engine (@repo/core-storage)
This package provides an Offline-First Storage Engine using PouchDB, tailored for Enterprise React applications. It is built to seamlessly sync with remote CouchDB instances, providing full fault tolerance and offline capabilities.
High-Level Overview
Our storage architecture enforces strict Inversion of Control (IoC). The core engine (@repo/core-storage) is a pure factory—it knows absolutely nothing about your business domains, data models, or specific databases. Consuming applications (like apps/web) dictate the rules by injecting their specific configurations and generic types into the storage engine.
graph TD
subgraph UI ["Consuming App (apps/*)"]
COMP["React Components / Forms"]
end
subgraph CoreStorage ["@repo/core-storage Engine"]
MGR["PouchDatabaseManager Factory"]
L_SALES[("Local PouchDB: Sales")]
L_INV[("Local PouchDB: Inventory")]
end
subgraph RemoteServer ["CouchDB Cluster (Cloud/On-Prem)"]
R_SALES[("Remote CouchDB: sales_db")]
R_INV[("Remote CouchDB: inventory_db")]
end
COMP -->|Read / Write| L_SALES
COMP -->|Read / Write| L_INV
MGR -->|Instantiates Multi-DB| L_SALES
MGR -->|Instantiates Multi-DB| L_INV
L_SALES <-->|Native Sync Live and Retry| R_SALES
L_INV <-->|Native Sync Live and Retry| R_INV
%% Styling Nodes
style MGR fill:#339af0,stroke:#1864ab,color:#fff
style L_SALES fill:#845ef7,stroke:#5f3dc4,color:#fff
style L_INV fill:#845ef7,stroke:#5f3dc4,color:#fff
style R_SALES fill:#fab005,stroke:#e67700,color:#fff
style R_INV fill:#fab005,stroke:#e67700,color:#fff
Core Concepts & Usage
1. Initialization & Registration (PouchDatabaseManager)
The PouchDatabaseManager acts as the IoC Factory. Apps use it to register and initialize multiple discrete PouchDB databases using a PouchConfig.
Why we use this pattern: Instead of scattering raw database instantiations across the codebase, the manager centralizes connections. If a database is requested twice, the manager efficiently returns the exact same instance.
import { PouchDatabaseManager } from '@repo/core-storage';
import type { Item } from './types';
export const dbManager = new PouchDatabaseManager();
// Register a strictly-typed database with bi-directional sync
export const itemDB = dbManager.register<Item>({
localName: 'items_db',
remoteUrl: 'http://admin:password@localhost:5984/items_db'
});
2. CRUD & Queries (PouchDatabaseWrapper)
When you register a database, you receive a strictly typed PouchDatabaseWrapper. This wrapper abstracts away the raw PouchDB API, giving developers clean, Promise-based helper methods without ever needing to pass dbName or complex identifiers repeatedly.
| Method | Description |
|---|---|
create(data) |
Inserts a new document. PouchDB will auto-generate an _id if omitted. |
update(id, data) |
Automatically fetches the latest _rev to merge the payload, preventing conflict errors. |
delete(id) |
Automatically fetches the latest _rev to safely remove the document. |
getOne(id) |
Retrieves a single document by its _id. |
getAll() |
Retrieves all documents, automatically filtering out internal _design/ docs. |
find(options) |
Queries using MongoDB-style selectors (via pouchdb-find). |
Example of find() with Selectors:
Instead of pulling all documents into memory and filtering them with JavaScript, we leverage native MongoDB-style selectors for performance:
const expensiveItems = await itemDB.find({
selector: {
price: { $gt: 100 },
category: 'electronics'
}
});
3. Real-Time Reactivity (The onChange Pub/Sub Pattern)
CRITICAL CONCEPT: We do not expose the raw db.changes() feed directly to React components. Instead, the PouchDatabaseWrapper utilizes a clean Pub/Sub abstraction via the .onChange(callback) method.
Why we use this pattern:
- Memory Safety: Direct bindings to PouchDB's raw changes feed often lead to zombie listeners and memory leaks. The
.onChange()returns an unsubscribe function natively tailored for React'suseEffectcleanup block. - Connection Efficiency: It maintains a single WebSocket/Polling connection to the database under the hood. Multiple React components can subscribe to the same wrapper without opening dozens of parallel database connections.
import { useEffect, useCallback, useState } from 'react';
import { itemDB } from '../core/db';
export function InventoryList() {
const [items, setItems] = useState([]);
const loadData = useCallback(async () => {
const data = await itemDB.getAll();
setItems(data);
}, []);
useEffect(() => {
// 1. Initial Load
loadData();
// 2. Subscribe to local mutations AND remote CouchDB syncs
const unsubscribe = itemDB.onChange(() => {
console.log('Database updated locally or remotely. Refreshing...');
loadData();
});
// 3. Prevent memory leaks!
return () => {
unsubscribe();
};
}, [loadData]);
// UI rendering...
}
4. CouchDB Sync & CORS Troubleshooting
By providing a remoteUrl to the manager, the engine automatically handles bi-directional synchronization in the background (live: true, retry: true). If the server goes down, the local app will continue working seamlessly and sync automatically when the connection is restored.
Warning
CORS Infinite Retries & Preflight Failures If your browser blocks the synchronization with a CORS error, you will see PouchDB enter an infinite retry loop in the network tab.
Do NOT try to fix this in the frontend code! This is exclusively a CouchDB server configuration issue. You must enable CORS directly on the CouchDB instance by editing its
local.inior using its dashboard configuration to allow origins, credentials, and headers.