141 lines
5.9 KiB
Markdown
141 lines
5.9 KiB
Markdown
# Enterprise Storage Engine (`@repo/core-storage`)
|
|
|
|
[← Back to Root](../../README.md)
|
|
|
|
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.
|
|
|
|
```mermaid
|
|
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.
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:**
|
|
1. **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's `useEffect` cleanup block.
|
|
2. **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.
|
|
|
|
```tsx
|
|
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.ini` or using its dashboard configuration to allow origins, credentials, and headers. |