Skip to content

Commit 05cded9

Browse files
committed
feat(cookbooks-app-react-state-replication): Create a cookbook for state replication
- Added TodoListItem component for individual todo items. - Created TodoListManager to manage the todo list state and handle CRUD operations. - Introduced TodoStats component to display statistics of the todo items. - Developed types for TodoItem and TodoList for better type safety. - Configured PouchDB for local and remote CouchDB replication. - Implemented event handling for CouchDB sync events. - Created Home, Profile, and Todo pages to structure the application. - Added routing and navigation using react-router. - Included SyncStatusMonitor to display sync status in the UI. - Updated package dependencies and added TypeScript configurations.
1 parent 6064123 commit 05cded9

48 files changed

Lines changed: 2161 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@equinor/fusion-framework-cookbook-app-react-state-replication": minor
3+
---
4+
5+
Introduced a new cookbook: `@equinor/fusion-framework-cookbook-app-react-state-replication`.
6+
7+
- Demonstrates state replication between React apps and CouchDB using the Fusion Framework state module with PouchDB.
8+
- Features include:
9+
- Real-time state synchronization with CouchDB.
10+
- Conflict resolution for concurrent edits.
11+
- Comprehensive examples for managing user profiles and todo lists.
12+
- Includes CouchDB management scripts for development:
13+
- `couchdb:start`, `couchdb:stop`, `couchdb:logs`, `couchdb:clean`.
14+
- Added detailed documentation and examples for multi-instance state synchronization.
15+
- Pages included:
16+
- **Home**: Overview of the cookbook, quick start guide, and learning resources.
17+
- **Profile**: Demonstrates user profile management with state replication.
18+
- **Todos**: A replicated todo list showcasing real-time synchronization and conflict resolution.
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
# Fusion Framework CouchDB State Replication Cookbook
2+
3+
A comprehensive cookbook demonstrating real-time state synchronization between React applications and CouchDB using the Fusion Framework state module with PouchDB replication.
4+
5+
## 🎯 Learning Objectives
6+
7+
After working through this cookbook, you will understand:
8+
9+
- How to configure CouchDB replication with the Fusion Framework state module
10+
- How to set up local CouchDB using Docker for development
11+
- How to implement bidirectional state synchronization between apps and databases
12+
- How to handle offline-first scenarios with automatic sync when online
13+
- Best practices for conflict resolution and error handling in distributed state
14+
15+
## 🏗️ Setup
16+
17+
### Prerequisites
18+
19+
- Node.js 18+
20+
- pnpm package manager
21+
- Docker and Docker Compose
22+
- Basic knowledge of React hooks and CouchDB concepts
23+
24+
### Quick Start
25+
26+
1. **Install dependencies:**
27+
```bash
28+
pnpm install
29+
```
30+
31+
2. **Start the development server:**
32+
```bash
33+
pnpm dev
34+
```
35+
36+
3. **Access CouchDB Admin UI:**
37+
- URL: http://localhost:5984/_utils
38+
- Username: `admin`
39+
- Password: `admin`
40+
41+
4. **Test the replication:**
42+
- Open the app in multiple browser tabs
43+
- Make changes on `profile` or `todos` page and watch them appear in others!
44+
45+
### Docker Commands
46+
47+
```bash
48+
# Start CouchDB
49+
pnpm couchdb:start
50+
51+
# Stop CouchDB
52+
pnpm couchdb:stop
53+
54+
# View CouchDB logs
55+
pnpm couchdb:logs
56+
57+
# Clean up (stop, remove container and volume)
58+
pnpm couchdb:clean
59+
```
60+
61+
## 📚 Key Concepts
62+
63+
### State Replication Architecture
64+
65+
This cookbook demonstrates a complete offline-first architecture:
66+
67+
```
68+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
69+
│ React App │ │ PouchDB │ │ CouchDB │
70+
│ │ │ (Local Store) │ │ (Remote DB) │
71+
│ ┌─────────────┐ │ │ │ │ │
72+
│ │ useAppState │◄┼────┼► Local Storage │◄───┼► Remote Storage │
73+
│ │ Hooks │ │ │ │ │ │
74+
│ └─────────────┘ │ │ • Offline-first │ │ • Persistence │
75+
│ │ │ • Instant UI │ │ • Multi-user │
76+
│ • UI Updates │ │ • Auto-sync │ │ • Backup │
77+
│ • User Actions │ │ • Conflict res. │ │ • HTTP API │
78+
└─────────────────┘ └──────────────────┘ └─────────────────┘
79+
```
80+
81+
### Configuration Overview
82+
83+
The application's CouchDB replication is configured in [`src/config.ts`](./src/config.ts). This file sets up both the local PouchDB instance (for offline-first storage) and the remote CouchDB database (for synchronization), and wires them into the Fusion Framework state module.
84+
85+
Key steps in the configuration:
86+
87+
- **Local Database:** A PouchDB instance named `cookbook_app_state` is created for browser storage with auto-compaction enabled.
88+
- **Remote Database:** A remote PouchDB instance connects to CouchDB at `http://localhost:5984/cookbooks_app_state` using admin credentials.
89+
- **Module Integration:** The custom state module is enabled with these databases, allowing automatic bidirectional sync.
90+
- **Navigation:** The navigation module is also enabled for multi-page support.
91+
92+
See the full configuration in [`src/config.ts`](./src/config.ts) for implementation details.
93+
94+
### State Management with useAppState
95+
96+
Use the `useAppState` hook just like React's `useState`, but with automatic persistence and replication:
97+
98+
```typescript
99+
import { useAppState } from '@equinor/fusion-framework-react-app/state';
100+
101+
const [state, setState] = useAppState<MyState>('my.state.key', {
102+
defaultValue: {
103+
value: '',
104+
updatedAt: new Date().toISOString(),
105+
},
106+
});
107+
108+
// Any updates to state are persisted and replicated automatically
109+
setState(prev => ({
110+
...prev,
111+
value: 'New Value',
112+
updatedAt: new Date().toISOString(),
113+
}));
114+
```
115+
116+
## 🔍 Code Structure
117+
118+
```text
119+
src/
120+
├── App.tsx # Main application entry point
121+
├── config.ts # Fusion Framework and CouchDB setup
122+
├── index.ts # App bootstrap
123+
├── components/
124+
│ ├── ProfileManager/ # Profile management
125+
│ ├── SyncStatus/ # Replication status and logs
126+
│ └── Todo/ # Todo list
127+
└── modules/
128+
└── app-state-with-replication/
129+
├── configurator.ts # Allow configuration of local and remote db
130+
├── provider.ts # Override base provider to setup sync
131+
├── module.ts # Override setup of configurator and provider
132+
└── observe-sync.ts # Helper to listen and aggregate sync events
133+
```
134+
135+
## 🧪 Examples in This Cookbook
136+
137+
### 1. Profile Manager
138+
139+
Demonstrates basic state management with CouchDB replication:
140+
141+
- **User profile data** - name, email, preferences
142+
- **Reducer pattern** - manage complex state transitions
143+
- **Real-time updates** - changes sync across browser tabs
144+
- **Optimistic updates** - UI responds immediately
145+
- **Conflict resolution** - last-write-wins strategy
146+
147+
### 2. Sync Status Monitor
148+
149+
Provides real-time monitoring of replication status:
150+
151+
- **Connection status** - online, offline, syncing, error
152+
- **Sync events log** - detailed replication activity
153+
- **Manual sync trigger** - force sync when needed
154+
- **Error handling** - graceful degradation on failures
155+
156+
**Visual Indicators:**
157+
- 🟢 Online - Connected and synced
158+
- 🔵 Syncing - Data transfer in progress
159+
- 🔴 Offline/Error - Connection issues
160+
161+
### 3. Todo List
162+
163+
Complex state management with real-world patterns:
164+
165+
- **CRUD operations** - create, read, update, delete todos
166+
- **Optimistic updates** - instant UI feedback
167+
- **Conflict resolution** - handles concurrent edits
168+
- **Data relationships** - nested object structures
169+
170+
## 📦 Module - App State With Replication
171+
172+
The `app-state-with-replication` module encapsulates the logic for integrating CouchDB replication into the Fusion Framework state system. Here’s a summary of its key components and how they work together:
173+
174+
- **`configurator.ts`**
175+
Provides a configurator for setting up both local (PouchDB) and remote (CouchDB) databases. It allows you to specify database names, URLs, and sync options, making the module flexible for different environments.
176+
177+
- **`provider.ts`**
178+
Extends the base state provider to initialize and manage replication. It sets up listeners for replication events (like sync, error, paused, active) and exposes sync status to the app, enabling UI components to react to connectivity and conflict states.
179+
180+
- **`module.ts`**
181+
Registers the custom configurator and provider with the Fusion Framework. This ensures that when the module is enabled, your app state is automatically wired for replication and sync monitoring.
182+
183+
- **`observe-sync.ts`**
184+
Implements utilities to observe and aggregate sync events from PouchDB. It provides hooks and helpers to track replication status, errors, and progress, which can be used to display real-time sync indicators in your UI.
185+
186+
- **`types.ts`**
187+
Defines TypeScript types for configuration, sync status, and events. This ensures type safety and clarity when working with the module’s APIs.
188+
189+
- **`enable-module.ts`**
190+
Exposes a helper to easily enable the module in your Fusion Framework app, streamlining integration.
191+
192+
- **`index.ts`**
193+
Entry point that exports the module’s public API, including hooks and configuration helpers.
194+
195+
**Key Takeaways:**
196+
- The module abstracts away the complexity of CouchDB/PouchDB replication, exposing a simple API for state management with robust sync.
197+
- Sync status and events are observable, allowing you to build responsive UIs that reflect real-time connectivity and conflict resolution.
198+
- Configuration is centralized and type-safe, making it easy to adapt for different deployment scenarios (dev, prod, per-user DBs, etc.).
199+
- The modular design means you can swap out or extend replication logic as your app’s needs evolve.
200+
201+
Refer to the [source files](src/modules/app-state-with-replication/) for implementation details and customization options.
202+
203+
## 🔧 Advanced Configuration
204+
205+
### Custom Sync Options
206+
207+
Fine-tune replication behavior:
208+
209+
```typescript
210+
const syncOptions = {
211+
live: true, // Enable continuous replication
212+
retry: true, // Retry on connection failure
213+
heartbeat: 10000, // Heartbeat interval (ms)
214+
timeout: 30000, // Request timeout (ms)
215+
batch_size: 100, // Batch size for bulk operations
216+
batches_limit: 10, // Maximum parallel batches
217+
filter: 'app/by_user', // Filter specific documents
218+
since: 'now', // Start replication from now
219+
};
220+
```
221+
222+
### Security Configuration
223+
224+
For production environments:
225+
226+
```typescript
227+
// Enable SSL/TLS for production
228+
const remoteDb = PouchDbStorage.CreateDb(remoteDbUrl, {
229+
fetch: (url, opts) => {
230+
return fetch(url, {
231+
...opts,
232+
headers: {
233+
...opts.headers,
234+
'Authorization': 'Bearer ' + authModule.getToken(),
235+
},
236+
});
237+
},
238+
});
239+
```
240+
241+
### Database Per User
242+
#### Per-User Database Routing Example
243+
244+
To route requests to a remote CouchDB instance and automatically prefix the database or document path with the current user's username, you can use an Express route as a proxy. This approach ensures each user's data is isolated in their own database or document namespace.
245+
246+
Example Express proxy route:
247+
248+
```typescript
249+
const httpProxy = require('http-proxy');
250+
const url = require('url');
251+
252+
// Create proxy server
253+
const proxy = httpProxy.createProxyServer({
254+
target: 'http://couch-db-remote:5984',
255+
changeOrigin: true
256+
});
257+
258+
// Handle proxy path rewriting
259+
proxy.on('proxyReq', (proxyReq, req, res) => {
260+
const parsedUrl = url.parse(req.url);
261+
const dbName = parsedUrl.pathname.split('/').pop();
262+
const userId = 'user123'; // Simulated user ID
263+
const newPath = `/${userId}_${dbName}${parsedUrl.search || ''}`;
264+
proxyReq.path = newPath;
265+
});
266+
```
267+
**Usage:**
268+
Mount this proxy route in your Express app.
269+
When you POST to `/proxy-to-remote/:dbName`, the route automatically prefixes the CouchDB database path with the current user's username, isolating each user's data.
270+
Example:
271+
A POST to `/proxy-to-remote/todos` for user `alice` will proxy to `/alice_todos` on CouchDB.
272+
273+
### Common Issues
274+
275+
1. **CouchDB Connection Refused**
276+
```bash
277+
# Check if CouchDB is running
278+
curl http://localhost:5984/
279+
280+
# Restart CouchDB
281+
pnpm couchdb:down && pnpm couchdb:up
282+
```
283+
284+
2. **CORS Errors**
285+
- Check browser developer tools for detailed error messages
286+
287+
3. **Authentication Errors**
288+
- Verify credentials: `admin` / `admin`
289+
- Check CouchDB admin interface: http://localhost:5984/_utils
290+
291+
## 💡 Best Practices
292+
293+
### 1. State Key Organization
294+
295+
Use hierarchical naming for better organization:
296+
297+
```typescript
298+
// ✅ Good - hierarchical, descriptive
299+
'user.profile.personal'
300+
'user.preferences.theme'
301+
'app.settings.notifications'
302+
'feature.dashboard.filters'
303+
304+
// ❌ Avoid - flat, unclear
305+
'userdata'
306+
'settings'
307+
'stuff'
308+
```
309+
310+
### 2. Use strong typing
311+
312+
```typescript
313+
// ✅ Good - strong typing
314+
interface UserProfile {
315+
id: string;
316+
name: string;
317+
email: string;
318+
}
319+
320+
const [user, setUser] = useAppState<UserProfile>('user.profile');
321+
322+
// ❌ Avoid - weak typing
323+
const [user, setUser] = useAppState('user.profile');
324+
```
325+
326+
### 3. Validate Complex Schemas
327+
```typescript
328+
329+
const userSchema = z.object({
330+
id: z.string().uuid(),
331+
name: z.string().min(2).max(100),
332+
email: z.string().email(),
333+
});
334+
335+
type UserProfile = z.infer<typeof userSchema>;
336+
337+
// ✅ Good - strong typing with validation
338+
const useMyUser = () => {
339+
const [value, setValue] = useAppState<UserProfile>('user.profile');
340+
const setUser = useCallback((user: UserProfile) => {
341+
if (userSchema.safeParse(user).success) {
342+
setValue(user);
343+
return true;
344+
} else {
345+
console.warn('Provided user is invalid');
346+
return false;
347+
}
348+
}, [setValue]);
349+
if(!userSchema.safeParse(value).success) {
350+
console.warn('Current user state is invalid');
351+
return null;
352+
}
353+
return value;
354+
};
355+
356+
const [user, setUser] = useMyUser();
357+
358+
// ❌ Avoid - no validation
359+
const [user, setUser] = useAppState<UserProfile>();
360+
console.log(value.name);

0 commit comments

Comments
 (0)