fix(core): Fix state initialization, hash detection, and validation - v2.0.25
This commit is contained in:
13
changelog.md
13
changelog.md
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-07-29 - 2.0.25 - fix(core)
|
||||
Major state initialization and validation improvements
|
||||
|
||||
- Fixed state hash bug: Now properly compares hash values instead of storing state objects
|
||||
- Fixed state initialization merge order: Initial state now correctly takes precedence over stored state
|
||||
- Improved type safety: stateStore properly typed as potentially undefined
|
||||
- Simplified init mode logic with clear behavior for 'soft', 'mandatory', 'force', and 'persistent'
|
||||
- Added state validation with extensible validateState() method
|
||||
- Made notifyChange() async to support proper hash comparison
|
||||
- Enhanced select() to filter undefined states automatically
|
||||
- Added comprehensive test suite for state initialization scenarios
|
||||
- Updated documentation with clearer examples and improved readme
|
||||
|
||||
## 2025-07-19 - 2.0.24 - fix(core)
|
||||
Multiple fixes and improvements
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartstate",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.25",
|
||||
"private": false,
|
||||
"description": "A package for handling and managing state in applications.",
|
||||
"main": "dist_ts/index.js",
|
||||
|
@@ -1,39 +1,52 @@
|
||||
# Smartstate Implementation Notes
|
||||
|
||||
## Current API (as of analysis)
|
||||
## Current API (as of v2.0.24+)
|
||||
|
||||
### State Part Initialization
|
||||
- State parts can be created with different init modes: 'soft', 'mandatory', 'force', 'persistent'
|
||||
- State parts can be created with different init modes: 'soft' (default), 'mandatory', 'force', 'persistent'
|
||||
- 'soft' - returns existing state part if exists, creates new if not
|
||||
- 'mandatory' - requires state part to not exist, fails if it does
|
||||
- 'force' - always creates new state part, overwriting any existing
|
||||
- 'persistent' - like 'soft' but with WebStore persistence (IndexedDB)
|
||||
- Persistent mode automatically calls init() internally - no need to call it manually
|
||||
- WebStore integration for persistent state uses IndexedDB
|
||||
- State merge order fixed: initial state takes precedence over stored state
|
||||
|
||||
### Actions
|
||||
- Actions are created with `createAction()` method
|
||||
- Two ways to dispatch actions:
|
||||
1. `stateAction.trigger(payload)` - returns Promise<TStatePayload>
|
||||
2. `await statePart.dispatchAction(stateAction, payload)` - returns Promise<TStatePayload>
|
||||
- Both methods now return the same Promise, providing flexibility in usage
|
||||
- Both methods return the same Promise, providing flexibility in usage
|
||||
|
||||
### State Management Methods
|
||||
- `select()` - returns Observable with startWith current state
|
||||
- `select()` - returns Observable with startWith current state, filters undefined states
|
||||
- `waitUntilPresent()` - waits for specific state condition
|
||||
- `stateSetup()` - async state initialization with cumulative defer
|
||||
- `notifyChangeCumulative()` - defers notification to end of call stack (no callback parameter)
|
||||
- `notifyChangeCumulative()` - defers notification to end of call stack
|
||||
- `getState()` - returns current state or undefined
|
||||
- `setState()` - validates state before setting, notifies only on actual changes
|
||||
|
||||
### State Hash Detection
|
||||
- Uses SHA256 hash to detect actual state changes
|
||||
- Bug: Currently stores the state object itself as hash instead of the actual hash
|
||||
- This prevents proper duplicate notification prevention
|
||||
- Fixed: Hash comparison now properly awaits async hash calculation
|
||||
- Prevents duplicate notifications for identical state values
|
||||
- `notifyChange()` is now async to support proper hash comparison
|
||||
|
||||
### State Validation
|
||||
- Basic validation ensures state is not null/undefined
|
||||
- `validateState()` method can be overridden in subclasses for custom validation
|
||||
- Validation runs on both setState() and when loading from persistent storage
|
||||
|
||||
### Type System
|
||||
- Can use either enums or string literal types for state part names
|
||||
- Test uses simple string types: `type TMyStateParts = 'testStatePart'`
|
||||
- State can be undefined initially, handled properly in select() and other methods
|
||||
|
||||
## Fixed Issues in Documentation
|
||||
1. Updated trigger() to return Promise (API enhancement)
|
||||
2. Added dispatchAction as alternative method
|
||||
3. Corrected notifyChangeCumulative usage
|
||||
4. Clarified persistent mode auto-init
|
||||
5. Added stateSetup documentation
|
||||
6. Fixed state hash detection description
|
||||
7. Both trigger() and dispatchAction() now return Promise for consistency
|
||||
## Recent Fixes (v2.0.24+)
|
||||
1. Fixed state hash bug - now properly compares hash values instead of promises
|
||||
2. Fixed state initialization merge order - initial state now takes precedence
|
||||
3. Ensured stateStore is properly typed as potentially undefined
|
||||
4. Simplified init mode logic with clear behavior for each mode
|
||||
5. Added state validation with extensible validateState() method
|
||||
6. Made notifyChange() async to support proper hash comparison
|
||||
7. Updated select() to filter undefined states
|
126
readme.md
126
readme.md
@@ -1,12 +1,19 @@
|
||||
# @push.rocks/smartstate
|
||||
A package for handling and managing state in applications
|
||||
A powerful TypeScript library for elegant state management using RxJS and reactive programming patterns
|
||||
|
||||
## Install
|
||||
|
||||
To install `@push.rocks/smartstate`, you can use pnpm (Performant Node Package Manager). Run the following command in your terminal:
|
||||
To install `@push.rocks/smartstate`, you can use pnpm, npm, or yarn:
|
||||
|
||||
```bash
|
||||
# Using pnpm (recommended)
|
||||
pnpm install @push.rocks/smartstate --save
|
||||
|
||||
# Using npm
|
||||
npm install @push.rocks/smartstate --save
|
||||
|
||||
# Using yarn
|
||||
yarn add @push.rocks/smartstate
|
||||
```
|
||||
|
||||
This will add `@push.rocks/smartstate` to your project's dependencies.
|
||||
@@ -35,10 +42,10 @@ const myAppSmartState = new Smartstate<YourStatePartNamesEnum>();
|
||||
|
||||
When creating state parts, you can specify different initialization modes:
|
||||
|
||||
- **`'soft'`** - Allows existing state parts to remain (default behavior)
|
||||
- **`'mandatory'`** - Fails if there's an existing state part with the same name
|
||||
- **`'force'`** - Overwrites any existing state part
|
||||
- **`'persistent'`** - Enables WebStore persistence using IndexedDB
|
||||
- **`'soft'`** (default) - Returns existing state part if it exists, creates new if not
|
||||
- **`'mandatory'`** - Requires state part to not exist, fails if it does
|
||||
- **`'force'`** - Always creates new state part, overwriting any existing one
|
||||
- **`'persistent'`** - Like 'soft' but with WebStore persistence using IndexedDB
|
||||
|
||||
### Defining State Parts
|
||||
|
||||
@@ -79,6 +86,7 @@ const userStatePart = await myAppSmartState.getStatePart<IUserState>(
|
||||
You can subscribe to changes in a state part to perform actions accordingly:
|
||||
|
||||
```typescript
|
||||
// The select() method automatically filters out undefined states
|
||||
userStatePart.select().subscribe((currentState) => {
|
||||
console.log(`User Logged In: ${currentState.isLoggedIn}`);
|
||||
});
|
||||
@@ -134,6 +142,12 @@ Both methods return a Promise with the new state, giving you flexibility in how
|
||||
`StatePart` provides several useful methods for state management:
|
||||
|
||||
```typescript
|
||||
// Get current state (may be undefined initially)
|
||||
const currentState = userStatePart.getState();
|
||||
if (currentState) {
|
||||
console.log('Current user:', currentState.username);
|
||||
}
|
||||
|
||||
// Wait for a specific state condition
|
||||
await userStatePart.waitUntilPresent();
|
||||
|
||||
@@ -170,13 +184,33 @@ Persistent state automatically:
|
||||
- Restores state on application restart
|
||||
- Manages storage with configurable database and store names
|
||||
|
||||
### State Validation
|
||||
|
||||
`Smartstate` includes built-in state validation to ensure data integrity:
|
||||
|
||||
```typescript
|
||||
// Basic validation (built-in)
|
||||
// Ensures state is not null or undefined
|
||||
await userStatePart.setState(null); // Throws error: Invalid state structure
|
||||
|
||||
// Custom validation by extending StatePart
|
||||
class ValidatedStatePart<T> extends StatePart<string, T> {
|
||||
protected validateState(stateArg: any): stateArg is T {
|
||||
// Add your custom validation logic
|
||||
return super.validateState(stateArg) && /* your validation */;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
`Smartstate` includes built-in performance optimizations:
|
||||
`Smartstate` includes advanced performance optimizations:
|
||||
|
||||
- **State Change Detection**: Detects actual state changes to prevent unnecessary notifications when state values haven't truly changed
|
||||
- **Async State Hash Detection**: Uses SHA256 hashing to detect actual state changes, preventing unnecessary notifications when state values haven't truly changed
|
||||
- **Duplicate Prevention**: Identical state updates are automatically filtered out
|
||||
- **Cumulative Notifications**: Batch multiple state changes into a single notification using `notifyChangeCumulative()`
|
||||
- **Selective Subscriptions**: Use selectors to subscribe only to specific state properties
|
||||
- **Undefined State Filtering**: The `select()` method automatically filters out undefined states
|
||||
|
||||
### RxJS Integration
|
||||
|
||||
@@ -202,19 +236,75 @@ userStatePart.select(state => state.username)
|
||||
});
|
||||
```
|
||||
|
||||
### Comprehensive Usage
|
||||
### Complete Example
|
||||
|
||||
Putting it all together, `@push.rocks/smartstate` offers a flexible and powerful pattern for managing application state. By modularizing state parts, subscribing to state changes, and controlling state modifications through actions, developers can maintain a clean and scalable architecture. Combining these strategies with persistent states unlocks the full potential for creating dynamic and user-friendly applications.
|
||||
Here's a comprehensive example showcasing the power of `@push.rocks/smartstate`:
|
||||
|
||||
Key features:
|
||||
- **Type-safe state management** with full TypeScript support
|
||||
- **Reactive state updates** using RxJS observables
|
||||
- **Persistent state** with IndexedDB storage
|
||||
- **Performance optimized** with state hash detection
|
||||
- **Modular architecture** with separate state parts
|
||||
- **Action-based updates** for predictable state modifications
|
||||
```typescript
|
||||
import { Smartstate, StatePart, StateAction } from '@push.rocks/smartstate';
|
||||
|
||||
For more complex scenarios, consider combining multiple state parts, creating hierarchical state structures, and integrating with other state management solutions as needed. With `@push.rocks/smartstate`, the possibilities are vast, empowering you to tailor the state management approach to fit the unique requirements of your project.
|
||||
// Define your state structure
|
||||
type AppStateParts = 'user' | 'settings' | 'cart';
|
||||
|
||||
interface IUserState {
|
||||
isLoggedIn: boolean;
|
||||
username?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
interface ICartState {
|
||||
items: Array<{ id: string; quantity: number }>;
|
||||
total: number;
|
||||
}
|
||||
|
||||
// Create the smartstate instance
|
||||
const appState = new Smartstate<AppStateParts>();
|
||||
|
||||
// Initialize state parts
|
||||
const userState = await appState.getStatePart<IUserState>('user', {
|
||||
isLoggedIn: false
|
||||
});
|
||||
|
||||
const cartState = await appState.getStatePart<ICartState>('cart', {
|
||||
items: [],
|
||||
total: 0
|
||||
}, 'persistent'); // Persists across sessions
|
||||
|
||||
// Create actions
|
||||
const loginAction = userState.createAction<{ username: string; email: string }>(
|
||||
async (statePart, payload) => {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
return {
|
||||
isLoggedIn: true,
|
||||
username: payload.username,
|
||||
email: payload.email
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Subscribe to changes
|
||||
userState.select(state => state.isLoggedIn).subscribe(isLoggedIn => {
|
||||
console.log('Login status changed:', isLoggedIn);
|
||||
});
|
||||
|
||||
// Dispatch actions
|
||||
await loginAction.trigger({ username: 'john', email: 'john@example.com' });
|
||||
```
|
||||
|
||||
### Key Features
|
||||
|
||||
`@push.rocks/smartstate` provides a robust foundation for state management:
|
||||
|
||||
- **🎯 Type-safe** - Full TypeScript support with intelligent type inference
|
||||
- **⚡ Performance optimized** - Async state hash detection prevents unnecessary re-renders
|
||||
- **💾 Persistent state** - Built-in IndexedDB support for state persistence
|
||||
- **🔄 Reactive** - Powered by RxJS for elegant async handling
|
||||
- **🧩 Modular** - Organize state into logical, reusable parts
|
||||
- **✅ Validated** - Built-in state validation with extensible validation logic
|
||||
- **🎭 Flexible init modes** - Choose how state parts are initialized
|
||||
- **📦 Zero config** - Works out of the box with sensible defaults
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
|
@@ -55,4 +55,4 @@ tap.test('should dispatch a state action', async (tools) => {
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
tap.start();
|
||||
export default tap.start();
|
||||
|
157
test/test.initialization.both.ts
Normal file
157
test/test.initialization.both.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import * as smartstate from '../ts/index.js';
|
||||
|
||||
type TTestStateParts = 'initTest' | 'persistTest' | 'forceTest';
|
||||
|
||||
interface ITestState {
|
||||
value: number;
|
||||
nested: {
|
||||
data: string;
|
||||
};
|
||||
}
|
||||
|
||||
tap.test('should handle soft init mode (default)', async () => {
|
||||
const state = new smartstate.Smartstate<TTestStateParts>();
|
||||
|
||||
// First creation
|
||||
const statePart1 = await state.getStatePart<ITestState>('initTest', {
|
||||
value: 1,
|
||||
nested: { data: 'initial' }
|
||||
});
|
||||
expect(statePart1.getState()).toEqual({
|
||||
value: 1,
|
||||
nested: { data: 'initial' }
|
||||
});
|
||||
|
||||
// Second call should return existing
|
||||
const statePart2 = await state.getStatePart<ITestState>('initTest');
|
||||
expect(statePart1).toEqual(statePart2);
|
||||
});
|
||||
|
||||
tap.test('should handle mandatory init mode', async () => {
|
||||
const state = new smartstate.Smartstate<TTestStateParts>();
|
||||
|
||||
// First creation should succeed
|
||||
const statePart1 = await state.getStatePart<ITestState>('initTest', {
|
||||
value: 1,
|
||||
nested: { data: 'initial' }
|
||||
}, 'mandatory');
|
||||
expect(statePart1).toBeInstanceOf(smartstate.StatePart);
|
||||
|
||||
// Second call with mandatory should fail
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await state.getStatePart<ITestState>('initTest', {
|
||||
value: 2,
|
||||
nested: { data: 'second' }
|
||||
}, 'mandatory');
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
expect(error).not.toBeNull();
|
||||
expect(error?.message).toMatch(/already exists.*mandatory/);
|
||||
});
|
||||
|
||||
tap.test('should handle force init mode', async () => {
|
||||
const state = new smartstate.Smartstate<TTestStateParts>();
|
||||
|
||||
// First creation
|
||||
const statePart1 = await state.getStatePart<ITestState>('forceTest', {
|
||||
value: 1,
|
||||
nested: { data: 'initial' }
|
||||
});
|
||||
expect(statePart1.getState()?.value).toEqual(1);
|
||||
|
||||
// Force should create new state part
|
||||
const statePart2 = await state.getStatePart<ITestState>('forceTest', {
|
||||
value: 2,
|
||||
nested: { data: 'forced' }
|
||||
}, 'force');
|
||||
expect(statePart2.getState()?.value).toEqual(2);
|
||||
expect(statePart1).not.toEqual(statePart2);
|
||||
});
|
||||
|
||||
tap.test('should handle missing initial state error', async () => {
|
||||
const state = new smartstate.Smartstate<TTestStateParts>();
|
||||
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await state.getStatePart<ITestState>('initTest');
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
expect(error).not.toBeNull();
|
||||
expect(error?.message).toMatch(/does not exist.*no initial state/);
|
||||
});
|
||||
|
||||
tap.test('should handle state validation', async () => {
|
||||
const state = new smartstate.Smartstate<TTestStateParts>();
|
||||
|
||||
const statePart = await state.getStatePart<ITestState>('initTest', {
|
||||
value: 1,
|
||||
nested: { data: 'initial' }
|
||||
});
|
||||
|
||||
// Setting null should fail validation
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await statePart.setState(null as any);
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
expect(error).not.toBeNull();
|
||||
expect(error?.message).toMatch(/Invalid state structure/);
|
||||
});
|
||||
|
||||
tap.test('should handle undefined state in select', async () => {
|
||||
const state = new smartstate.Smartstate<TTestStateParts>();
|
||||
const statePart = new smartstate.StatePart<TTestStateParts, ITestState>('initTest');
|
||||
|
||||
// Select should filter out undefined states
|
||||
const values: (ITestState | undefined)[] = [];
|
||||
statePart.select().subscribe(val => values.push(val));
|
||||
|
||||
// Initially undefined, should not emit
|
||||
expect(values).toHaveLength(0);
|
||||
|
||||
// After setting state, should emit
|
||||
await statePart.setState({
|
||||
value: 1,
|
||||
nested: { data: 'test' }
|
||||
});
|
||||
|
||||
expect(values).toHaveLength(1);
|
||||
expect(values[0]).toEqual({
|
||||
value: 1,
|
||||
nested: { data: 'test' }
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('should not notify on duplicate state', async () => {
|
||||
const state = new smartstate.Smartstate<TTestStateParts>();
|
||||
const statePart = await state.getStatePart<ITestState>('initTest', {
|
||||
value: 1,
|
||||
nested: { data: 'initial' }
|
||||
});
|
||||
|
||||
let notificationCount = 0;
|
||||
// Use select() to get initial value + changes
|
||||
statePart.select().subscribe(() => notificationCount++);
|
||||
|
||||
// Should have received initial state
|
||||
expect(notificationCount).toEqual(1);
|
||||
|
||||
// Set same state multiple times
|
||||
await statePart.setState({ value: 1, nested: { data: 'initial' } });
|
||||
await statePart.setState({ value: 1, nested: { data: 'initial' } });
|
||||
await statePart.setState({ value: 1, nested: { data: 'initial' } });
|
||||
|
||||
// Should still be 1 (no new notifications for duplicate state)
|
||||
expect(notificationCount).toEqual(1);
|
||||
|
||||
// Change state should notify
|
||||
await statePart.setState({ value: 2, nested: { data: 'changed' } });
|
||||
expect(notificationCount).toEqual(2);
|
||||
});
|
||||
|
||||
export default tap.start();
|
@@ -13,9 +13,10 @@ export class Smartstate<StatePartNameType extends string> {
|
||||
|
||||
/**
|
||||
* Allows getting and initializing a new statepart
|
||||
* initMode === 'soft' it will allow existing stateparts
|
||||
* initMode === 'mandatory' will fail if there is an existing statepart
|
||||
* initMode === 'force' will overwrite any existing statepart
|
||||
* initMode === 'soft' (default) - returns existing statepart if exists, creates new if not
|
||||
* initMode === 'mandatory' - requires statepart to not exist, fails if it does
|
||||
* initMode === 'force' - always creates new statepart, overwriting any existing
|
||||
* initMode === 'persistent' - like 'soft' but with webstore persistence
|
||||
* @param statePartNameArg
|
||||
* @param initialArg
|
||||
* @param initMode
|
||||
@@ -23,19 +24,30 @@ export class Smartstate<StatePartNameType extends string> {
|
||||
public async getStatePart<PayloadType>(
|
||||
statePartNameArg: StatePartNameType,
|
||||
initialArg?: PayloadType,
|
||||
initMode?: TInitMode
|
||||
initMode: TInitMode = 'soft'
|
||||
): Promise<StatePart<StatePartNameType, PayloadType>> {
|
||||
if (this.statePartMap[statePartNameArg]) {
|
||||
if (initialArg && (!initMode || initMode !== 'soft')) {
|
||||
throw new Error(
|
||||
`${statePartNameArg} already exists, yet you try to set an initial state again`
|
||||
);
|
||||
const existingStatePart = this.statePartMap[statePartNameArg];
|
||||
|
||||
if (existingStatePart) {
|
||||
switch (initMode) {
|
||||
case 'mandatory':
|
||||
throw new Error(
|
||||
`State part '${statePartNameArg}' already exists, but initMode is 'mandatory'`
|
||||
);
|
||||
case 'force':
|
||||
// Force mode: create new state part
|
||||
return this.createStatePart<PayloadType>(statePartNameArg, initialArg, initMode);
|
||||
case 'soft':
|
||||
case 'persistent':
|
||||
default:
|
||||
// Return existing state part
|
||||
return existingStatePart as StatePart<StatePartNameType, PayloadType>;
|
||||
}
|
||||
return this.statePartMap[statePartNameArg] as StatePart<StatePartNameType, PayloadType>;
|
||||
} else {
|
||||
// State part doesn't exist
|
||||
if (!initialArg) {
|
||||
throw new Error(
|
||||
`${statePartNameArg} does not yet exist, yet you don't provide an initial state`
|
||||
`State part '${statePartNameArg}' does not exist and no initial state provided`
|
||||
);
|
||||
}
|
||||
return this.createStatePart<PayloadType>(statePartNameArg, initialArg, initMode);
|
||||
@@ -46,11 +58,12 @@ export class Smartstate<StatePartNameType extends string> {
|
||||
* Creates a statepart
|
||||
* @param statePartName
|
||||
* @param initialPayloadArg
|
||||
* @param initMode
|
||||
*/
|
||||
private async createStatePart<PayloadType>(
|
||||
statePartName: StatePartNameType,
|
||||
initialPayloadArg: PayloadType,
|
||||
initMode?: TInitMode
|
||||
initMode: TInitMode = 'soft'
|
||||
): Promise<StatePart<StatePartNameType, PayloadType>> {
|
||||
const newState = new StatePart<StatePartNameType, PayloadType>(
|
||||
statePartName,
|
||||
@@ -64,8 +77,8 @@ export class Smartstate<StatePartNameType extends string> {
|
||||
await newState.init();
|
||||
const currentState = newState.getState();
|
||||
await newState.setState({
|
||||
...initialPayloadArg,
|
||||
...currentState,
|
||||
...initialPayloadArg,
|
||||
});
|
||||
this.statePartMap[statePartName] = newState;
|
||||
return newState;
|
||||
|
@@ -4,7 +4,7 @@ import { StateAction, type IActionDef } from './smartstate.classes.stateaction.j
|
||||
export class StatePart<TStatePartName, TStatePayload> {
|
||||
public name: TStatePartName;
|
||||
public state = new plugins.smartrx.rxjs.Subject<TStatePayload>();
|
||||
public stateStore: TStatePayload;
|
||||
public stateStore: TStatePayload | undefined;
|
||||
private cumulativeDeferred = plugins.smartpromise.cumulativeDefer();
|
||||
|
||||
private webStoreOptions: plugins.webstore.IWebStoreOptions;
|
||||
@@ -27,9 +27,9 @@ export class StatePart<TStatePartName, TStatePayload> {
|
||||
this.webStore = new plugins.webstore.WebStore<TStatePayload>(this.webStoreOptions);
|
||||
await this.webStore.init();
|
||||
const storedState = await this.webStore.get(String(this.name));
|
||||
if (storedState) {
|
||||
if (storedState && this.validateState(storedState)) {
|
||||
this.stateStore = storedState;
|
||||
this.notifyChange();
|
||||
await this.notifyChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export class StatePart<TStatePartName, TStatePayload> {
|
||||
/**
|
||||
* gets the state from the state store
|
||||
*/
|
||||
public getState(): TStatePayload {
|
||||
public getState(): TStatePayload | undefined {
|
||||
return this.stateStore;
|
||||
}
|
||||
|
||||
@@ -46,8 +46,13 @@ export class StatePart<TStatePartName, TStatePayload> {
|
||||
* @param newStateArg
|
||||
*/
|
||||
public async setState(newStateArg: TStatePayload) {
|
||||
// Validate state structure
|
||||
if (!this.validateState(newStateArg)) {
|
||||
throw new Error(`Invalid state structure for state part '${this.name}'`);
|
||||
}
|
||||
|
||||
this.stateStore = newStateArg;
|
||||
this.notifyChange();
|
||||
await this.notifyChange();
|
||||
|
||||
// Save state to WebStore if initialized
|
||||
if (this.webStore) {
|
||||
@@ -56,21 +61,34 @@ export class StatePart<TStatePartName, TStatePayload> {
|
||||
return this.stateStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates state structure - can be overridden for custom validation
|
||||
* @param stateArg
|
||||
*/
|
||||
protected validateState(stateArg: any): stateArg is TStatePayload {
|
||||
// Basic validation - ensure state is not null/undefined
|
||||
// Subclasses can override for more specific validation
|
||||
return stateArg !== null && stateArg !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* notifies of a change on the state
|
||||
*/
|
||||
public notifyChange() {
|
||||
const createStateHash = (stateArg: any) => {
|
||||
return plugins.smarthashWeb.sha256FromString(plugins.smartjson.stringify(stateArg));
|
||||
public async notifyChange() {
|
||||
if (!this.stateStore) {
|
||||
return;
|
||||
}
|
||||
const createStateHash = async (stateArg: any) => {
|
||||
return await plugins.smarthashWeb.sha256FromString(plugins.smartjson.stringify(stateArg));
|
||||
};
|
||||
const currentHash = await createStateHash(this.stateStore);
|
||||
if (
|
||||
this.stateStore &&
|
||||
this.lastStateNotificationPayloadHash &&
|
||||
createStateHash(this.stateStore) === this.lastStateNotificationPayloadHash
|
||||
currentHash === this.lastStateNotificationPayloadHash
|
||||
) {
|
||||
return;
|
||||
} else {
|
||||
this.lastStateNotificationPayloadHash = this.stateStore;
|
||||
this.lastStateNotificationPayloadHash = currentHash;
|
||||
}
|
||||
this.state.next(this.stateStore);
|
||||
}
|
||||
@@ -81,7 +99,11 @@ export class StatePart<TStatePartName, TStatePayload> {
|
||||
*/
|
||||
public notifyChangeCumulative() {
|
||||
// TODO: check viability
|
||||
setTimeout(() => this.state.next(this.stateStore), 0);
|
||||
setTimeout(async () => {
|
||||
if (this.stateStore) {
|
||||
await this.notifyChange();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,6 +117,7 @@ export class StatePart<TStatePartName, TStatePayload> {
|
||||
}
|
||||
const mapped = this.state.pipe(
|
||||
plugins.smartrx.rxjs.ops.startWith(this.getState()),
|
||||
plugins.smartrx.rxjs.ops.filter((stateArg): stateArg is TStatePayload => stateArg !== undefined),
|
||||
plugins.smartrx.rxjs.ops.map((stateArg) => {
|
||||
try {
|
||||
return selectorFn(stateArg);
|
||||
|
Reference in New Issue
Block a user