Skip to content

Commit 4d0e955

Browse files
committed
feat: add state management module with sync capabilities and React hooks
- Add @equinor/fusion-framework-module-state with PouchDB sync storage - Add state management support to @equinor/fusion-framework-app - Add useAppState hook to @equinor/fusion-framework-react-app - Add state management cookbooks for examples and replication demos - Reorganize event system with comprehensive state event types - Update dependencies and fix formatting issues
1 parent caaa767 commit 4d0e955

36 files changed

Lines changed: 1573 additions & 473 deletions
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ Add state management module support to @equinor/fusion-framework-app
66

77
This change introduces comprehensive state management capabilities to the app package, allowing developers to easily enable persistent state storage in their Fusion applications.
88

9-
### New Features
9+
**Features**
1010

1111
- **State Module Enabler**: Added `enableState` function that configures the state module with PouchDB storage
1212
- **Package Exports**: Added new export path `./enable-state` for the state enabler function
1313
- **Type Definitions**: Added typesVersions support for the new state enabler
1414
- **Peer Dependencies**: Added `@equinor/fusion-framework-module-state` as an optional peer dependency
1515
- **Storage Configuration**: Automatic app-scoped storage with key prefixing to prevent state collisions
1616

17-
### Usage
17+
**Usage**
1818

1919
Applications can now enable state management by:
2020

File renamed without changes.

.changeset/add-app-react-state-cookbook.md renamed to .changeset/cookbook-app-react-state_examples.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
"@equinor/fusion-framework-cookbook-app-react-state": major
33
---
44

5-
**@equinor/fusion-framework-cookbook-app-react-state:**
6-
75
🎉 Added new cookbook demonstrating state management with useAppState hook in React applications!
86

97
This comprehensive cookbook 📚 provides developers with practical examples and best practices for using the Fusion Framework state module in React applications.
@@ -12,7 +10,7 @@ The cookbook includes complete state management examples demonstrating simple st
1210

1311
Key features include:
1412
- 📖 Educational documentation with learning objectives, setup instructions, and key concepts
15-
- 🔷 Full TypeScript implementation with proper type definitions
13+
- 🔷 Full TypeScript implementation with proper type definitions
1614
- ⚡ Complete development setup with dev server and build configuration
1715

1816
Developers will learn how to configure the state module using `enableAppState`, use the `useAppState` hook for persistent state management, share state between components with unique state keys, manage complex object state with proper update patterns, and follow best practices for organizing state in Fusion applications.

.changeset/improve-app-configurator-architecture.md

Lines changed: 0 additions & 30 deletions
This file was deleted.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
"@equinor/fusion-framework-module-state": major
3+
---
4+
5+
**🚀 Introducing the Fusion Framework State Module**
6+
7+
**@equinor/fusion-framework-module-state** is a new reactive state management module with built-in synchronization capabilities and comprehensive event system for enterprise-grade applications.
8+
9+
**🗄️ Synchronization Features**
10+
- **PouchDbSyncStorage**: Bidirectional synchronization with remote databases
11+
- **Real-time sync events**: Monitor sync progress, errors, and status changes
12+
- **Conflict resolution**: Automatic handling of concurrent data modifications
13+
- **Live sync options**: Configurable heartbeat, retry, and timeout settings
14+
15+
**📡 Event System**
16+
Comprehensive event architecture for type-safe state management:
17+
18+
**Event Classes:**
19+
- `StateEntryCreatedEvent` - Item creation tracking
20+
- `StateEntryUpdatedEvent` - Item modification events
21+
- `StateEntryDeletedEvent` - Item removal notifications
22+
- `StateSyncChangeEvent` - Sync data changes
23+
- `StateSyncCompleteEvent` - Sync operation completion
24+
- `StateSyncErrorEvent` - Sync failure handling
25+
- `StateSyncStatusEvent` - Sync status monitoring
26+
- `StateOperationSuccessEvent` - Successful operations
27+
- `StateOperationFailureEvent` - Operation failure tracking
28+
29+
**Event Organization:**
30+
- `StateChangeEvent` - CRUD operation events
31+
- `StateSyncEvent` - Synchronization events
32+
- `StateOperationEvent` - Operation result events
33+
34+
**🏗️ Core Architecture**
35+
- **StateProvider**: Reactive state management with observable patterns
36+
- **Storage Interface**: Extensible storage backends with sync capabilities
37+
- **Type Safety**: Comprehensive TypeScript definitions
38+
- **Memory Management**: Proper cleanup and disposal patterns

.changeset/implement-react-app-state-management.md renamed to .changeset/react-app_useAppState.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ We're excited to bring you a complete state management solution that makes shari
99
**What's new?** The powerful `useAppState` hook works just like React's `useState` but with superpowers:
1010

1111
🔄 **Persistent by default** - Your state survives page refreshes using browser storage
12-
🔗 **Automatically synchronized** - Share state between any components in real-time
12+
🔗 **Automatically synchronized** - Share state between any components in real-time
1313
**Optimistically updated** - Lightning-fast UI with automatic error recovery
1414
🛡️ **Fully type-safe** - Complete TypeScript support with type inference
1515
🎯 **Dead simple API** - If you know `useState`, you already know `useAppState`

cookbooks/app-react-state-replication/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"@equinor/fusion-framework-cli": "workspace:^",
2424
"@equinor/fusion-framework-module": "workspace:^",
2525
"@equinor/fusion-framework-module-event": "workspace:^",
26-
"@equinor/fusion-framework-module-navigation": "^5.0.3",
26+
"@equinor/fusion-framework-module-navigation": "workspace:^",
2727
"@equinor/fusion-framework-module-state": "workspace:^",
2828
"@equinor/fusion-framework-react-app": "workspace:^",
2929
"@equinor/fusion-observable": "workspace:^",

cookbooks/app-react-state-replication/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"outDir": "./dist",
55
"rootDir": "./src",
66
"jsx": "react-jsx",
7-
"baseUrl": "./src",
7+
"baseUrl": "./src"
88
},
99
"references": [{ "path": "../../packages/modules/state" }],
1010
"include": ["src/**/*"],

packages/modules/state/README.md

Lines changed: 147 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,16 @@ const modules = await configurator.initialize();
2929

3030
// Use the state provider
3131
const stateProvider = modules.state;
32+
33+
// Store and retrieve data
3234
await stateProvider.storeItem({ key: 'example', value: 'Hello, World!' });
3335
const item = await stateProvider.getItem('example');
3436
console.log(item?.value); // "Hello, World!"
37+
38+
// Observe changes reactively
39+
stateProvider.observeItem('example').subscribe(item => {
40+
console.log('Item updated:', item?.value);
41+
});
3542
```
3643

3744
## Module Setup
@@ -304,16 +311,16 @@ import { PouchDbStorage } from '@equinor/fusion-framework-module-state/storage';
304311

305312
enableStateModule(configurator, async (builder) => {
306313
// Create local PouchDB storage
307-
const localStorage = PouchDbStorage.CreateDb('my-app-state');
308-
const remoteStorage = PouchDbStorage.CreateDb('http://localhost:5984/my-app-state');
314+
const localDb = PouchDbStorage.CreateDb('my-app-state');
315+
const remoteDb = PouchDbStorage.CreateDb('http://localhost:5984/my-app-state');
309316

310317
// Set up two-way sync with remote CouchDB
311-
localStorage.sync(remoteStorage, {
318+
localDb.sync(remoteDb, {
312319
live: true,
313320
retry: true
314321
});
315322

316-
builder.setStorage(new PouchDbStorage(localStorage));
323+
builder.setStorage(new PouchDbStorage(localDb));
317324
});
318325
```
319326

@@ -322,6 +329,25 @@ enableStateModule(configurator, async (builder) => {
322329
- **`retry: true`**: Automatically retries replication on connection failures
323330
- **`filter`**: Apply custom filters to replicate only specific documents
324331
- **`since`**: Start replication from a specific sequence number
332+
333+
### Monitoring Sync Progress
334+
335+
The state module provides comprehensive sync event monitoring through RxJS observables:
336+
337+
```typescript
338+
import { enableStateModule } from '@equinor/fusion-framework-module-state';
339+
import { StateSyncEvent } from '@equinor/fusion-framework-module-state/events';
340+
341+
enableStateModule(configurator, async (builder) => {
342+
const localDb = PouchDbStorage.CreateDb('my-app-state');
343+
const remoteDb = PouchDbStorage.CreateDb('http://localhost:5984/my-app-state');
344+
345+
// Start sync
346+
const sync = localDb.sync(remoteDb, { live: true, retry: true });
347+
348+
builder.setStorage(new PouchDbStorage(localDb));
349+
});
350+
325351
### Custom Storage Implementation
326352

327353
If you need to use a different storage backend, you can create your own by implementing the `IStorage` interface. This allows you to integrate with any storage system (e.g., custom APIs, browser storage, or other databases).
@@ -535,6 +561,123 @@ Response object for bulk item retrieval operations.
535561
- **`offset?: number`**
536562
- Current offset in the result set.
537563

564+
## Testing
565+
566+
The state module includes comprehensive test utilities and examples:
567+
568+
```typescript
569+
import { StateProvider } from '@equinor/fusion-framework-module-state';
570+
import { createMockStorage } from '@equinor/fusion-framework-module-state/__tests__/storage.mock';
571+
572+
// Create a mock storage for testing
573+
const mockStorage = createMockStorage();
574+
const stateProvider = new StateProvider({ storage: mockStorage });
575+
576+
// Test your state operations
577+
describe('State Management', () => {
578+
it('should store and retrieve items', async () => {
579+
await stateProvider.storeItem({ key: 'test', value: 'data' });
580+
const item = await stateProvider.getItem('test');
581+
expect(item?.value).toBe('data');
582+
});
583+
});
584+
```
585+
586+
## Migration Guide
587+
588+
### Upgrading from v1.x to v2.x
589+
590+
**Breaking Changes:**
591+
- Event system has been refactored - see [events documentation](#monitoring-sync-progress)
592+
- Storage interface methods are now async by default
593+
- `observeItems` now returns `StateItem<T>[]` instead of `StateItem[]`
594+
595+
**Migration Steps:**
596+
1. Update event subscriptions to use new event types
597+
2. Add `await` to storage operations if using custom storage
598+
3. Update type annotations for `observeItems` return values
599+
600+
### From Other State Libraries
601+
602+
**Redux Migration:**
603+
```typescript
604+
// Before (Redux)
605+
const store = createStore(reducer);
606+
store.dispatch({ type: 'SET_DATA', payload: data });
607+
608+
// After (Fusion State)
609+
await stateProvider.storeItem({ key: 'data', value: data });
610+
```
611+
612+
**Zustand Migration:**
613+
```typescript
614+
// Before (Zustand)
615+
const useStore = create((set) => ({ data, setData }));
616+
useStore.getState().setData(newData);
617+
618+
// After (Fusion State)
619+
await stateProvider.storeItem({ key: 'data', value: newData });
620+
```
621+
622+
## Performance Considerations
623+
624+
### Optimization Tips
625+
626+
1. **Use prefix filtering** for large datasets:
627+
```typescript
628+
// Good: Filter by prefix for better performance
629+
const userPrefs = await stateProvider.getAllItems({ prefix: 'user.' });
630+
631+
// Avoid: Loading all items when you only need a subset
632+
const allItems = await stateProvider.getAllItems();
633+
const userPrefs = allItems.items.filter(item => item.key.startsWith('user.'));
634+
```
635+
636+
2. **Batch operations** for multiple changes:
637+
```typescript
638+
// Good: Single bulk operation
639+
await stateProvider.storeItems([
640+
{ key: 'user.name', value: 'John' },
641+
{ key: 'user.email', value: 'john@example.com' }
642+
]);
643+
644+
// Avoid: Multiple individual operations
645+
await stateProvider.storeItem({ key: 'user.name', value: 'John' });
646+
await stateProvider.storeItem({ key: 'user.email', value: 'john@example.com' });
647+
```
648+
649+
3. **Unsubscribe from observables** when components unmount:
650+
```typescript
651+
// Good: Proper cleanup
652+
useEffect(() => {
653+
const subscription = stateProvider.observeItem('data').subscribe(setData);
654+
return () => subscription.unsubscribe();
655+
}, []);
656+
657+
// Avoid: Memory leaks
658+
stateProvider.observeItem('data').subscribe(setData); // Never unsubscribed
659+
```
660+
661+
### Memory Management
662+
663+
- **PouchDB Storage**: Uses IndexedDB in browsers, which has storage limits
664+
- **Memory Storage**: All data kept in memory - lost on page refresh
665+
- **Custom Storage**: Depends on your implementation
666+
667+
### Sync Performance
668+
669+
- **Live sync**: Provides real-time updates but increases network usage
670+
- **Batch sync**: Reduces network calls but may show stale data
671+
- **Filtered sync**: Only sync relevant data to improve performance
672+
538673
## Development
539674

540675
This package is part of the [Fusion Framework monorepo](https://github.com/equinor/fusion-framework).
676+
677+
### Contributing
678+
679+
We welcome contributions! Please see our [contributing guidelines](../../CONTRIBUTING.md) for details on:
680+
- Code style and formatting
681+
- Testing requirements
682+
- Pull request process
683+
- Release process

packages/modules/state/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
"import": "./dist/esm/index.js",
1111
"types": "./dist/types/index.d.ts"
1212
},
13+
"./events": {
14+
"import": "./dist/esm/events/index.js",
15+
"types": "./dist/types/storeventsage/index.d.ts"
16+
},
1317
"./storage": {
1418
"import": "./dist/esm/storage/index.js",
1519
"types": "./dist/types/storage/index.d.ts"

0 commit comments

Comments
 (0)