Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 07bfbfd393 | |||
| aa411072f2 | |||
| 02575e8baf | |||
| 09fc53aaff | |||
| bcb58dd012 | |||
| f0064bd94b | |||
| e9c527a9dc | |||
| a47d8bb3c7 | |||
| aa6766ef36 | |||
| b0442e1227 | |||
| 0f1eb6eb27 | |||
| dd18ef94bd | |||
| a0189921a5 | |||
| b409f1aa55 |
@@ -6,8 +6,8 @@ on:
|
|||||||
- '**'
|
- '**'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@code.foss.global/${{gitea.repository}}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Install pnpm and npmci
|
- name: Install pnpm and npmci
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
|
|
||||||
- name: Run npm prepare
|
- name: Run npm prepare
|
||||||
run: npmci npm prepare
|
run: npmci npm prepare
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ on:
|
|||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@code.foss.global/${{gitea.repository}}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Audit production dependencies
|
- name: Audit production dependencies
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Test stable
|
- name: Test stable
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Code quality
|
- name: Code quality
|
||||||
|
|||||||
67
changelog.md
67
changelog.md
@@ -1,5 +1,72 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-08-16 - 2.0.26 - fix(ci)
|
||||||
|
Add local Claude settings file to allow helper permissions for common local commands
|
||||||
|
|
||||||
|
- Added .claude/settings.local.json to grant local helper permissions for tooling
|
||||||
|
- Allowed commands: Bash(tsx:*), Bash(tstest test:*), Bash(git add:*), Bash(git tag:*)
|
||||||
|
- No changes to source code or runtime behavior; tooling/config only
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- Fixed StateAction trigger method to properly return Promise<TStateType>
|
||||||
|
- Updated CI workflows to use new container registry and npmci package name
|
||||||
|
- Added pnpm workspace configuration for built-only dependencies
|
||||||
|
|
||||||
|
## 2025-07-19 - 2.0.23 - fix(ci)
|
||||||
|
Update CI workflows to use new container registry and npmci package name
|
||||||
|
|
||||||
|
- Changed CI image from 'registry.gitlab.com/hosttoday/ht-docker-node:npmci' to 'code.foss.global/host.today/ht-docker-node:npmci'
|
||||||
|
- Replaced npmci installation command from '@shipzone/npmci' to '@ship.zone/npmci' in workflow configurations
|
||||||
|
|
||||||
|
## 2025-07-19 - 2.0.22 - fix(smartstate)
|
||||||
|
Fix StateAction trigger method to properly return Promise
|
||||||
|
|
||||||
|
- Fixed StateAction.trigger() to return Promise<TStateType> as expected
|
||||||
|
- Updated readme with improved documentation and examples
|
||||||
|
- Replaced outdated legal information with Task Venture Capital GmbH details
|
||||||
|
- Added implementation notes in readme.hints.md
|
||||||
|
|
||||||
|
## 2025-06-19 - 2.0.21 - maintenance
|
||||||
|
General updates and improvements
|
||||||
|
|
||||||
|
## 2025-06-19 - 2.0.20 - fix(smartstate)
|
||||||
|
Update build scripts and dependency versions; replace isohash with smarthashWeb for state hash generation
|
||||||
|
|
||||||
|
- Adjusted package.json scripts to include verbose testing and modified build command
|
||||||
|
- Bumped development dependencies (tsbuild, tsbundle, tsrun, tstest, tapbundle) to newer versions
|
||||||
|
- Updated production dependencies (lik, smarthash, smartpromise, smartrx) with minor version bumps
|
||||||
|
- Replaced import of isohash with smarthashWeb in state hash generation, ensuring consistency across modules
|
||||||
|
|
||||||
|
## 2024-10-02 - 2.0.19 - fix(dependencies)
|
||||||
|
Update dependencies to latest versions
|
||||||
|
|
||||||
|
- Updated @git.zone/tsbuild to version ^2.1.84
|
||||||
|
- Updated @git.zone/tsbundle to version ^2.0.15
|
||||||
|
- Updated @git.zone/tsrun to version ^1.2.49
|
||||||
|
- Updated @git.zone/tstest to version ^1.0.90
|
||||||
|
- Updated @push.rocks/tapbundle to version ^5.3.0
|
||||||
|
- Updated @types/node to version ^22.7.4
|
||||||
|
- Updated @push.rocks/lik to version ^6.0.15
|
||||||
|
- Updated @push.rocks/smartjson to version ^5.0.20
|
||||||
|
- Updated @push.rocks/smartpromise to version ^4.0.4
|
||||||
|
- Updated @push.rocks/smartrx to version ^3.0.7
|
||||||
|
- Updated @push.rocks/webstore to version ^2.0.20
|
||||||
|
|
||||||
## 2024-10-02 - 2.0.18 - fix(core)
|
## 2024-10-02 - 2.0.18 - fix(core)
|
||||||
Fix type errors and typos in Smartstate class
|
Fix type errors and typos in Smartstate class
|
||||||
|
|
||||||
|
|||||||
33
package.json
33
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartstate",
|
"name": "@push.rocks/smartstate",
|
||||||
"version": "2.0.18",
|
"version": "2.0.26",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A package for handling and managing state in applications.",
|
"description": "A package for handling and managing state in applications.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@@ -9,25 +9,25 @@
|
|||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/)",
|
"test": "(tstest test/ --verbose)",
|
||||||
"build": "(tsbuild --web --allowimplicitany && tsbundle npm)",
|
"build": "(tsbuild tsfolders --allowimplicitany && tsbundle npm)",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.70",
|
"@git.zone/tsbuild": "^2.6.4",
|
||||||
"@git.zone/tsbundle": "^2.0.8",
|
"@git.zone/tsbundle": "^2.4.0",
|
||||||
"@git.zone/tsrun": "^1.2.46",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
"@git.zone/tstest": "^1.0.81",
|
"@git.zone/tstest": "^2.3.1",
|
||||||
"@push.rocks/tapbundle": "^5.0.15",
|
"@push.rocks/tapbundle": "^6.0.3",
|
||||||
"@types/node": "^20.8.0"
|
"@types/node": "^22.7.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/isohash": "^2.0.1",
|
"@push.rocks/lik": "^6.2.2",
|
||||||
"@push.rocks/lik": "^6.0.5",
|
"@push.rocks/smarthash": "^3.2.0",
|
||||||
"@push.rocks/smartjson": "^5.0.10",
|
"@push.rocks/smartjson": "^5.0.20",
|
||||||
"@push.rocks/smartpromise": "^4.0.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartrx": "^3.0.6",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/webstore": "^2.0.13"
|
"@push.rocks/webstore": "^2.0.20"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
@@ -60,5 +60,6 @@
|
|||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://code.foss.global/push.rocks/smartstate.git"
|
"url": "https://code.foss.global/push.rocks/smartstate.git"
|
||||||
}
|
},
|
||||||
|
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
|
||||||
}
|
}
|
||||||
|
|||||||
12630
pnpm-lock.yaml
generated
12630
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
4
pnpm-workspace.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
|
- mongodb-memory-server
|
||||||
|
- puppeteer
|
||||||
@@ -1 +1,52 @@
|
|||||||
|
# Smartstate Implementation Notes
|
||||||
|
|
||||||
|
## Current API (as of v2.0.24+)
|
||||||
|
|
||||||
|
### State Part Initialization
|
||||||
|
- 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
|
||||||
|
- 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 return the same Promise, providing flexibility in usage
|
||||||
|
|
||||||
|
### State Management Methods
|
||||||
|
- `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
|
||||||
|
- `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
|
||||||
|
- 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
|
||||||
|
|
||||||
|
## 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
|
||||||
217
readme.md
217
readme.md
@@ -1,12 +1,19 @@
|
|||||||
# @push.rocks/smartstate
|
# @push.rocks/smartstate
|
||||||
a package that handles state in a good way
|
A powerful TypeScript library for elegant state management using RxJS and reactive programming patterns
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
To install `@push.rocks/smartstate`, you can use npm (Node Package Manager). Run the following command in your terminal:
|
To install `@push.rocks/smartstate`, you can use pnpm, npm, or yarn:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Using pnpm (recommended)
|
||||||
|
pnpm install @push.rocks/smartstate --save
|
||||||
|
|
||||||
|
# Using npm
|
||||||
npm install @push.rocks/smartstate --save
|
npm install @push.rocks/smartstate --save
|
||||||
|
|
||||||
|
# Using yarn
|
||||||
|
yarn add @push.rocks/smartstate
|
||||||
```
|
```
|
||||||
|
|
||||||
This will add `@push.rocks/smartstate` to your project's dependencies.
|
This will add `@push.rocks/smartstate` to your project's dependencies.
|
||||||
@@ -31,17 +38,30 @@ import { Smartstate, StatePart, StateAction } from '@push.rocks/smartstate';
|
|||||||
const myAppSmartState = new Smartstate<YourStatePartNamesEnum>();
|
const myAppSmartState = new Smartstate<YourStatePartNamesEnum>();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Understanding Init Modes
|
||||||
|
|
||||||
|
When creating state parts, you can specify different initialization modes:
|
||||||
|
|
||||||
|
- **`'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
|
### Defining State Parts
|
||||||
|
|
||||||
State parts represent separable sections of your state, making it easier to manage and modularize. For example, you may have a state part for user data and another for application settings.
|
State parts represent separable sections of your state, making it easier to manage and modularize. For example, you may have a state part for user data and another for application settings.
|
||||||
|
|
||||||
Define an enum for state part names for better management:
|
Define state part names using either enums or string literal types:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
// Option 1: Using enums
|
||||||
enum AppStateParts {
|
enum AppStateParts {
|
||||||
UserState,
|
UserState = 'UserState',
|
||||||
SettingsState
|
SettingsState = 'SettingsState'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Option 2: Using string literal types (simpler approach)
|
||||||
|
type AppStateParts = 'UserState' | 'SettingsState';
|
||||||
```
|
```
|
||||||
|
|
||||||
Now, let's create a state part within our `myAppSmartState` instance:
|
Now, let's create a state part within our `myAppSmartState` instance:
|
||||||
@@ -54,8 +74,11 @@ interface IUserState {
|
|||||||
|
|
||||||
const userStatePart = await myAppSmartState.getStatePart<IUserState>(
|
const userStatePart = await myAppSmartState.getStatePart<IUserState>(
|
||||||
AppStateParts.UserState,
|
AppStateParts.UserState,
|
||||||
{ isLoggedIn: false } // Initial state
|
{ isLoggedIn: false }, // Initial state
|
||||||
|
'soft' // Init mode (optional, defaults to 'soft')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Note: Persistent state parts are automatically initialized internally
|
||||||
```
|
```
|
||||||
|
|
||||||
### Subscribing to State Changes
|
### Subscribing to State Changes
|
||||||
@@ -63,6 +86,7 @@ const userStatePart = await myAppSmartState.getStatePart<IUserState>(
|
|||||||
You can subscribe to changes in a state part to perform actions accordingly:
|
You can subscribe to changes in a state part to perform actions accordingly:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
// The select() method automatically filters out undefined states
|
||||||
userStatePart.select().subscribe((currentState) => {
|
userStatePart.select().subscribe((currentState) => {
|
||||||
console.log(`User Logged In: ${currentState.isLoggedIn}`);
|
console.log(`User Logged In: ${currentState.isLoggedIn}`);
|
||||||
});
|
});
|
||||||
@@ -93,29 +117,194 @@ const loginUserAction = userStatePart.createAction<ILoginPayload>(async (statePa
|
|||||||
|
|
||||||
// Dispatch the action to update the state
|
// Dispatch the action to update the state
|
||||||
loginUserAction.trigger({ username: 'johnDoe' });
|
loginUserAction.trigger({ username: 'johnDoe' });
|
||||||
|
// or await the result
|
||||||
|
const newState = await loginUserAction.trigger({ username: 'johnDoe' });
|
||||||
```
|
```
|
||||||
|
|
||||||
### Persistent State
|
### Dispatching Actions
|
||||||
|
|
||||||
`Smartstate` supports the concept of persistent states, where you can maintain state across sessions. To utilize this, specify a persistent mode when getting a state part:
|
There are two ways to dispatch actions:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const settingsStatePart = await myAppSmartState.getStatePart<AppStateParts, ISettingsState>(
|
// Method 1: Using trigger on the action (returns promise)
|
||||||
|
const newState = await loginUserAction.trigger({ username: 'johnDoe' });
|
||||||
|
// or fire and forget
|
||||||
|
loginUserAction.trigger({ username: 'johnDoe' });
|
||||||
|
|
||||||
|
// Method 2: Using dispatchAction on the state part (returns promise)
|
||||||
|
const newState = await userStatePart.dispatchAction(loginUserAction, { username: 'johnDoe' });
|
||||||
|
```
|
||||||
|
|
||||||
|
Both methods return a Promise with the new state, giving you flexibility in how you handle the result.
|
||||||
|
|
||||||
|
### Additional State Methods
|
||||||
|
|
||||||
|
`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();
|
||||||
|
|
||||||
|
// Wait for a specific property to be present
|
||||||
|
await userStatePart.waitUntilPresent(state => state.username);
|
||||||
|
|
||||||
|
// Setup initial state with async operations
|
||||||
|
await userStatePart.stateSetup(async (statePart) => {
|
||||||
|
// Perform async initialization
|
||||||
|
const userData = await fetchUserData();
|
||||||
|
return { ...statePart.getState(), ...userData };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Defer notification to end of call stack
|
||||||
|
userStatePart.notifyChangeCumulative();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Persistent State with WebStore
|
||||||
|
|
||||||
|
`Smartstate` supports persistent states using WebStore (IndexedDB-based storage), allowing you to maintain state across sessions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const settingsStatePart = await myAppSmartState.getStatePart<ISettingsState>(
|
||||||
AppStateParts.SettingsState,
|
AppStateParts.SettingsState,
|
||||||
{ theme: 'light' }, // Initial state
|
{ theme: 'light' }, // Initial state
|
||||||
'persistent' // Mode
|
'persistent' // Mode
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Note: init() is called automatically for persistent mode
|
||||||
```
|
```
|
||||||
|
|
||||||
This mode ensures that the state is saved and can be reloaded even after the application restarts, providing a seamless user experience.
|
Persistent state automatically:
|
||||||
|
- Saves state changes to IndexedDB
|
||||||
|
- Restores state on application restart
|
||||||
|
- Manages storage with configurable database and store names
|
||||||
|
|
||||||
### Comprehensive Usage
|
### State Validation
|
||||||
|
|
||||||
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.
|
`Smartstate` includes built-in state validation to ensure data integrity:
|
||||||
|
|
||||||
Remember to leverage TypeScript for its excellent support for types and interfaces, enhancing your development experience with type checking and IntelliSense, ensuring a more reliable and maintainable codebase.
|
```typescript
|
||||||
|
// Basic validation (built-in)
|
||||||
|
// Ensures state is not null or undefined
|
||||||
|
await userStatePart.setState(null); // Throws error: Invalid state structure
|
||||||
|
|
||||||
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.
|
// 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 advanced performance optimizations:
|
||||||
|
|
||||||
|
- **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
|
||||||
|
|
||||||
|
`Smartstate` leverages RxJS for reactive state management:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// State is exposed as an RxJS Subject
|
||||||
|
const stateObservable = userStatePart.select();
|
||||||
|
|
||||||
|
// Automatically starts with current state value
|
||||||
|
stateObservable.subscribe((state) => {
|
||||||
|
console.log('Current state:', state);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use selectors for specific properties
|
||||||
|
userStatePart.select(state => state.username)
|
||||||
|
.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
filter(username => username !== undefined)
|
||||||
|
)
|
||||||
|
.subscribe(username => {
|
||||||
|
console.log('Username changed:', username);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete Example
|
||||||
|
|
||||||
|
Here's a comprehensive example showcasing the power of `@push.rocks/smartstate`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Smartstate, StatePart, StateAction } from '@push.rocks/smartstate';
|
||||||
|
|
||||||
|
// 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
|
## License and Legal Information
|
||||||
|
|
||||||
|
|||||||
@@ -55,4 +55,4 @@ tap.test('should dispatch a state action', async (tools) => {
|
|||||||
await done.promise;
|
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();
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartstate',
|
name: '@push.rocks/smartstate',
|
||||||
version: '2.0.18',
|
version: '2.0.26',
|
||||||
description: 'A package for handling and managing state in applications.'
|
description: 'A package for handling and managing state in applications.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ export class Smartstate<StatePartNameType extends string> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows getting and initializing a new statepart
|
* Allows getting and initializing a new statepart
|
||||||
* initMode === 'soft' it will allow existing stateparts
|
* initMode === 'soft' (default) - returns existing statepart if exists, creates new if not
|
||||||
* initMode === 'mandatory' will fail if there is an existing statepart
|
* initMode === 'mandatory' - requires statepart to not exist, fails if it does
|
||||||
* initMode === 'force' will overwrite any existing statepart
|
* initMode === 'force' - always creates new statepart, overwriting any existing
|
||||||
|
* initMode === 'persistent' - like 'soft' but with webstore persistence
|
||||||
* @param statePartNameArg
|
* @param statePartNameArg
|
||||||
* @param initialArg
|
* @param initialArg
|
||||||
* @param initMode
|
* @param initMode
|
||||||
@@ -23,19 +24,30 @@ export class Smartstate<StatePartNameType extends string> {
|
|||||||
public async getStatePart<PayloadType>(
|
public async getStatePart<PayloadType>(
|
||||||
statePartNameArg: StatePartNameType,
|
statePartNameArg: StatePartNameType,
|
||||||
initialArg?: PayloadType,
|
initialArg?: PayloadType,
|
||||||
initMode?: TInitMode
|
initMode: TInitMode = 'soft'
|
||||||
): Promise<StatePart<StatePartNameType, PayloadType>> {
|
): Promise<StatePart<StatePartNameType, PayloadType>> {
|
||||||
if (this.statePartMap[statePartNameArg]) {
|
const existingStatePart = this.statePartMap[statePartNameArg];
|
||||||
if (initialArg && (!initMode || initMode !== 'soft')) {
|
|
||||||
|
if (existingStatePart) {
|
||||||
|
switch (initMode) {
|
||||||
|
case 'mandatory':
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${statePartNameArg} already exists, yet you try to set an initial state again`
|
`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 {
|
} else {
|
||||||
|
// State part doesn't exist
|
||||||
if (!initialArg) {
|
if (!initialArg) {
|
||||||
throw new Error(
|
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);
|
return this.createStatePart<PayloadType>(statePartNameArg, initialArg, initMode);
|
||||||
@@ -46,11 +58,12 @@ export class Smartstate<StatePartNameType extends string> {
|
|||||||
* Creates a statepart
|
* Creates a statepart
|
||||||
* @param statePartName
|
* @param statePartName
|
||||||
* @param initialPayloadArg
|
* @param initialPayloadArg
|
||||||
|
* @param initMode
|
||||||
*/
|
*/
|
||||||
private async createStatePart<PayloadType>(
|
private async createStatePart<PayloadType>(
|
||||||
statePartName: StatePartNameType,
|
statePartName: StatePartNameType,
|
||||||
initialPayloadArg: PayloadType,
|
initialPayloadArg: PayloadType,
|
||||||
initMode?: TInitMode
|
initMode: TInitMode = 'soft'
|
||||||
): Promise<StatePart<StatePartNameType, PayloadType>> {
|
): Promise<StatePart<StatePartNameType, PayloadType>> {
|
||||||
const newState = new StatePart<StatePartNameType, PayloadType>(
|
const newState = new StatePart<StatePartNameType, PayloadType>(
|
||||||
statePartName,
|
statePartName,
|
||||||
@@ -64,8 +77,8 @@ export class Smartstate<StatePartNameType extends string> {
|
|||||||
await newState.init();
|
await newState.init();
|
||||||
const currentState = newState.getState();
|
const currentState = newState.getState();
|
||||||
await newState.setState({
|
await newState.setState({
|
||||||
...initialPayloadArg,
|
|
||||||
...currentState,
|
...currentState,
|
||||||
|
...initialPayloadArg,
|
||||||
});
|
});
|
||||||
this.statePartMap[statePartName] = newState;
|
this.statePartMap[statePartName] = newState;
|
||||||
return newState;
|
return newState;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export class StateAction<TStateType, TActionPayloadType> {
|
|||||||
public actionDef: IActionDef<TStateType, TActionPayloadType>
|
public actionDef: IActionDef<TStateType, TActionPayloadType>
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public trigger(payload: TActionPayloadType) {
|
public trigger(payload: TActionPayloadType): Promise<TStateType> {
|
||||||
this.statePartRef.dispatchAction(this, payload);
|
return this.statePartRef.dispatchAction(this, payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { StateAction, type IActionDef } from './smartstate.classes.stateaction.j
|
|||||||
export class StatePart<TStatePartName, TStatePayload> {
|
export class StatePart<TStatePartName, TStatePayload> {
|
||||||
public name: TStatePartName;
|
public name: TStatePartName;
|
||||||
public state = new plugins.smartrx.rxjs.Subject<TStatePayload>();
|
public state = new plugins.smartrx.rxjs.Subject<TStatePayload>();
|
||||||
public stateStore: TStatePayload;
|
public stateStore: TStatePayload | undefined;
|
||||||
private cumulativeDeferred = plugins.smartpromise.cumulativeDefer();
|
private cumulativeDeferred = plugins.smartpromise.cumulativeDefer();
|
||||||
|
|
||||||
private webStoreOptions: plugins.webstore.IWebStoreOptions;
|
private webStoreOptions: plugins.webstore.IWebStoreOptions;
|
||||||
@@ -27,9 +27,9 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|||||||
this.webStore = new plugins.webstore.WebStore<TStatePayload>(this.webStoreOptions);
|
this.webStore = new plugins.webstore.WebStore<TStatePayload>(this.webStoreOptions);
|
||||||
await this.webStore.init();
|
await this.webStore.init();
|
||||||
const storedState = await this.webStore.get(String(this.name));
|
const storedState = await this.webStore.get(String(this.name));
|
||||||
if (storedState) {
|
if (storedState && this.validateState(storedState)) {
|
||||||
this.stateStore = 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
|
* gets the state from the state store
|
||||||
*/
|
*/
|
||||||
public getState(): TStatePayload {
|
public getState(): TStatePayload | undefined {
|
||||||
return this.stateStore;
|
return this.stateStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,8 +46,13 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|||||||
* @param newStateArg
|
* @param newStateArg
|
||||||
*/
|
*/
|
||||||
public async setState(newStateArg: TStatePayload) {
|
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.stateStore = newStateArg;
|
||||||
this.notifyChange();
|
await this.notifyChange();
|
||||||
|
|
||||||
// Save state to WebStore if initialized
|
// Save state to WebStore if initialized
|
||||||
if (this.webStore) {
|
if (this.webStore) {
|
||||||
@@ -56,21 +61,34 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|||||||
return this.stateStore;
|
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
|
* notifies of a change on the state
|
||||||
*/
|
*/
|
||||||
public notifyChange() {
|
public async notifyChange() {
|
||||||
const createStateHash = (stateArg: any) => {
|
if (!this.stateStore) {
|
||||||
return plugins.isohash.sha256FromString(plugins.smartjson.stringify(stateArg));
|
return;
|
||||||
|
}
|
||||||
|
const createStateHash = async (stateArg: any) => {
|
||||||
|
return await plugins.smarthashWeb.sha256FromString(plugins.smartjson.stringify(stateArg));
|
||||||
};
|
};
|
||||||
|
const currentHash = await createStateHash(this.stateStore);
|
||||||
if (
|
if (
|
||||||
this.stateStore &&
|
|
||||||
this.lastStateNotificationPayloadHash &&
|
this.lastStateNotificationPayloadHash &&
|
||||||
createStateHash(this.stateStore) === this.lastStateNotificationPayloadHash
|
currentHash === this.lastStateNotificationPayloadHash
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
this.lastStateNotificationPayloadHash = this.stateStore;
|
this.lastStateNotificationPayloadHash = currentHash;
|
||||||
}
|
}
|
||||||
this.state.next(this.stateStore);
|
this.state.next(this.stateStore);
|
||||||
}
|
}
|
||||||
@@ -81,7 +99,11 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|||||||
*/
|
*/
|
||||||
public notifyChangeCumulative() {
|
public notifyChangeCumulative() {
|
||||||
// TODO: check viability
|
// 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(
|
const mapped = this.state.pipe(
|
||||||
plugins.smartrx.rxjs.ops.startWith(this.getState()),
|
plugins.smartrx.rxjs.ops.startWith(this.getState()),
|
||||||
|
plugins.smartrx.rxjs.ops.filter((stateArg): stateArg is TStatePayload => stateArg !== undefined),
|
||||||
plugins.smartrx.rxjs.ops.map((stateArg) => {
|
plugins.smartrx.rxjs.ops.map((stateArg) => {
|
||||||
try {
|
try {
|
||||||
return selectorFn(stateArg);
|
return selectorFn(stateArg);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as isohash from '@push.rocks/isohash';
|
import * as smarthashWeb from '@push.rocks/smarthash/web';
|
||||||
import * as smartjson from '@push.rocks/smartjson';
|
import * as smartjson from '@push.rocks/smartjson';
|
||||||
import * as smartpromise from '@push.rocks/smartpromise';
|
import * as smartpromise from '@push.rocks/smartpromise';
|
||||||
import * as smartrx from '@push.rocks/smartrx';
|
import * as smartrx from '@push.rocks/smartrx';
|
||||||
import * as webstore from '@push.rocks/webstore';
|
import * as webstore from '@push.rocks/webstore';
|
||||||
|
|
||||||
export { isohash, smartjson, smartpromise, smartrx, webstore };
|
export { smarthashWeb, smartjson, smartpromise, smartrx, webstore };
|
||||||
|
|||||||
Reference in New Issue
Block a user