Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39aa63bdb3 | |||
| c1aa4eae5e | |||
| d8decdb3e5 | |||
| 03cfee2003 | |||
| f6a3e71f0a | |||
| 6436370abc |
31
changelog.md
31
changelog.md
@@ -1,5 +1,36 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-27 - 2.0.31 - fix(deps)
|
||||||
|
bump devDependencies and fix README license path
|
||||||
|
|
||||||
|
- Bump @git.zone/tsbundle from ^2.8.3 to ^2.9.0
|
||||||
|
- Bump @types/node from ^25.2.0 to ^25.3.2
|
||||||
|
- Update documented dependency set/version to v2.0.30 in readme.hints.md
|
||||||
|
- Fix README license file path from LICENSE to license in readme.md
|
||||||
|
|
||||||
|
## 2026-02-02 - 2.0.30 - fix(config)
|
||||||
|
update npmextra configuration and improve README: rename package keys, add release registry config, clarify waitUntilPresent timeout and notification/persistence behavior
|
||||||
|
|
||||||
|
- Renamed npmextra keys: 'gitzone' → '@git.zone/cli' and 'tsdoc' → '@git.zone/tsdoc'
|
||||||
|
- Added release configuration for @git.zone/cli including registries (verdaccio and npm) and accessLevel
|
||||||
|
- Removed top-level 'npmci' section
|
||||||
|
- Added new '@ship.zone/szci' entry with npmGlobalTools
|
||||||
|
- README: added waitUntilPresent timeout example with error handling
|
||||||
|
- README: clarified notifyChangeCumulative is debounced and documented persistence behavior (merge with defaults, atomic writes)
|
||||||
|
- README: documented concurrency/race-condition safety and timeout support for waitUntilPresent
|
||||||
|
|
||||||
|
## 2026-02-02 - 2.0.29 - fix(smartstate)
|
||||||
|
prevent duplicate statepart creation and fix persistence/notification race conditions
|
||||||
|
|
||||||
|
- Add pendingStatePartCreation map to deduplicate concurrent createStatePart calls
|
||||||
|
- Adjust init handling so 'force' falls through to creation and concurrent creations are serialized
|
||||||
|
- Merge persisted state with initial payload in 'persistent' initMode, with persisted values taking precedence
|
||||||
|
- Persist to WebStore before updating in-memory state to ensure atomicity
|
||||||
|
- Debounce cumulative notifications via pendingCumulativeNotification to avoid duplicate notifications
|
||||||
|
- Log selector errors instead of silently swallowing exceptions
|
||||||
|
- Add optional timeout to waitUntilPresent and ensure subscriptions and timeouts are cleaned up to avoid indefinite waits
|
||||||
|
- Await setState when performing chained state updates to ensure ordering and avoid race conditions
|
||||||
|
|
||||||
## 2026-02-02 - 2.0.28 - fix(deps)
|
## 2026-02-02 - 2.0.28 - fix(deps)
|
||||||
bump devDependencies and dependencies, add tsbundle build config, update docs, and reorganize tests
|
bump devDependencies and dependencies, add tsbundle build config, update docs, and reorganize tests
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
{
|
{
|
||||||
"npmci": {
|
|
||||||
"npmGlobalTools": [],
|
|
||||||
"npmAccessLevel": "public"
|
|
||||||
},
|
|
||||||
"@git.zone/tsbundle": {
|
"@git.zone/tsbundle": {
|
||||||
"bundles": [
|
"bundles": [
|
||||||
{
|
{
|
||||||
@@ -14,7 +10,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"gitzone": {
|
"@git.zone/cli": {
|
||||||
"projectType": "npm",
|
"projectType": "npm",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "code.foss.global",
|
"githost": "code.foss.global",
|
||||||
@@ -35,9 +31,19 @@
|
|||||||
"asynchronous state",
|
"asynchronous state",
|
||||||
"cumulative notification"
|
"cumulative notification"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"registries": [
|
||||||
|
"https://verdaccio.lossless.digital",
|
||||||
|
"https://registry.npmjs.org"
|
||||||
|
],
|
||||||
|
"accessLevel": "public"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tsdoc": {
|
"@git.zone/tsdoc": {
|
||||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||||
|
},
|
||||||
|
"@ship.zone/szci": {
|
||||||
|
"npmGlobalTools": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartstate",
|
"name": "@push.rocks/smartstate",
|
||||||
"version": "2.0.28",
|
"version": "2.0.31",
|
||||||
"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",
|
||||||
@@ -15,11 +15,11 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.1.2",
|
"@git.zone/tsbuild": "^4.1.2",
|
||||||
"@git.zone/tsbundle": "^2.8.3",
|
"@git.zone/tsbundle": "^2.9.0",
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@git.zone/tstest": "^3.1.8",
|
"@git.zone/tstest": "^3.1.8",
|
||||||
"@push.rocks/tapbundle": "^6.0.3",
|
"@push.rocks/tapbundle": "^6.0.3",
|
||||||
"@types/node": "^25.2.0"
|
"@types/node": "^25.3.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/lik": "^6.2.2",
|
"@push.rocks/lik": "^6.2.2",
|
||||||
|
|||||||
9918
pnpm-lock.yaml
generated
Normal file
9918
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -51,10 +51,10 @@
|
|||||||
6. Made notifyChange() async to support proper hash comparison
|
6. Made notifyChange() async to support proper hash comparison
|
||||||
7. Updated select() to filter undefined states
|
7. Updated select() to filter undefined states
|
||||||
|
|
||||||
## Dependency Versions (v2.0.28)
|
## Dependency Versions (v2.0.30)
|
||||||
- @git.zone/tsbuild: ^4.1.2
|
- @git.zone/tsbuild: ^4.1.2
|
||||||
- @git.zone/tsbundle: ^2.8.3
|
- @git.zone/tsbundle: ^2.9.0
|
||||||
- @git.zone/tsrun: ^2.0.1
|
- @git.zone/tsrun: ^2.0.1
|
||||||
- @git.zone/tstest: ^3.1.8
|
- @git.zone/tstest: ^3.1.8
|
||||||
- @push.rocks/smartjson: ^6.0.0
|
- @push.rocks/smartjson: ^6.0.0
|
||||||
- @types/node: ^25.2.0
|
- @types/node: ^25.3.2
|
||||||
23
readme.md
23
readme.md
@@ -145,19 +145,26 @@ if (currentState) {
|
|||||||
console.log('Current user:', currentState.username);
|
console.log('Current user:', currentState.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for a specific state condition
|
// Wait for state to be present
|
||||||
await userStatePart.waitUntilPresent();
|
await userStatePart.waitUntilPresent();
|
||||||
|
|
||||||
// Wait for a specific property to be present
|
// Wait for a specific property to be present
|
||||||
await userStatePart.waitUntilPresent(state => state.username);
|
await userStatePart.waitUntilPresent(state => state.username);
|
||||||
|
|
||||||
|
// Wait with a timeout (throws error if condition not met within timeout)
|
||||||
|
try {
|
||||||
|
await userStatePart.waitUntilPresent(state => state.username, 5000); // 5 second timeout
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Timed out waiting for username');
|
||||||
|
}
|
||||||
|
|
||||||
// Setup initial state with async operations
|
// Setup initial state with async operations
|
||||||
await userStatePart.stateSetup(async (statePart) => {
|
await userStatePart.stateSetup(async (statePart) => {
|
||||||
const userData = await fetchUserData();
|
const userData = await fetchUserData();
|
||||||
return { ...statePart.getState(), ...userData };
|
return { ...statePart.getState(), ...userData };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Defer notification to end of call stack
|
// Defer notification to end of call stack (debounced)
|
||||||
userStatePart.notifyChangeCumulative();
|
userStatePart.notifyChangeCumulative();
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -168,7 +175,7 @@ userStatePart.notifyChangeCumulative();
|
|||||||
```typescript
|
```typescript
|
||||||
const settingsStatePart = await myAppSmartState.getStatePart<ISettingsState>(
|
const settingsStatePart = await myAppSmartState.getStatePart<ISettingsState>(
|
||||||
AppStateParts.SettingsState,
|
AppStateParts.SettingsState,
|
||||||
{ theme: 'light' }, // Initial state
|
{ theme: 'light' }, // Initial/default state
|
||||||
'persistent' // Mode
|
'persistent' // Mode
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
@@ -176,7 +183,8 @@ const settingsStatePart = await myAppSmartState.getStatePart<ISettingsState>(
|
|||||||
Persistent state automatically:
|
Persistent state automatically:
|
||||||
- Saves state changes to IndexedDB
|
- Saves state changes to IndexedDB
|
||||||
- Restores state on application restart
|
- Restores state on application restart
|
||||||
- Manages storage with configurable database and store names
|
- Merges persisted values with defaults (persisted values take precedence)
|
||||||
|
- Ensures atomic writes (persistence happens before memory update)
|
||||||
|
|
||||||
### State Validation
|
### State Validation
|
||||||
|
|
||||||
@@ -200,9 +208,10 @@ class ValidatedStatePart<T> extends StatePart<string, T> {
|
|||||||
|
|
||||||
- **🔒 Async State Hash Detection**: Uses SHA256 hashing to detect actual state changes, preventing 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
|
- **🚫 Duplicate Prevention**: Identical state updates are automatically filtered out
|
||||||
- **📦 Cumulative Notifications**: Batch multiple state changes into a single notification using `notifyChangeCumulative()`
|
- **📦 Cumulative Notifications**: Batch multiple state changes into a single notification using `notifyChangeCumulative()` with automatic debouncing
|
||||||
- **🎯 Selective Subscriptions**: Use selectors to subscribe only to specific state properties
|
- **🎯 Selective Subscriptions**: Use selectors to subscribe only to specific state properties
|
||||||
- **✨ Undefined State Filtering**: The `select()` method automatically filters out undefined states
|
- **✨ Undefined State Filtering**: The `select()` method automatically filters out undefined states
|
||||||
|
- **⚡ Concurrent Access Safety**: Prevents race conditions when multiple calls request the same state part simultaneously
|
||||||
|
|
||||||
### RxJS Integration
|
### RxJS Integration
|
||||||
|
|
||||||
@@ -297,10 +306,12 @@ await loginAction.trigger({ username: 'john', email: 'john@example.com' });
|
|||||||
| ✅ **Validated** | Built-in state validation with extensible validation logic |
|
| ✅ **Validated** | Built-in state validation with extensible validation logic |
|
||||||
| 🎭 **Flexible init modes** | Choose how state parts are initialized |
|
| 🎭 **Flexible init modes** | Choose how state parts are initialized |
|
||||||
| 📦 **Zero config** | Works out of the box with sensible defaults |
|
| 📦 **Zero config** | Works out of the box with sensible defaults |
|
||||||
|
| 🛡️ **Race condition safe** | Concurrent state part creation is handled safely |
|
||||||
|
| ⏱️ **Timeout support** | `waitUntilPresent` supports optional timeouts |
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartstate',
|
name: '@push.rocks/smartstate',
|
||||||
version: '2.0.28',
|
version: '2.0.31',
|
||||||
description: 'A package for handling and managing state in applications.'
|
description: 'A package for handling and managing state in applications.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export type TInitMode = 'soft' | 'mandatory' | 'force' | 'persistent';
|
|||||||
export class Smartstate<StatePartNameType extends string> {
|
export class Smartstate<StatePartNameType extends string> {
|
||||||
public statePartMap: { [key in StatePartNameType]?: StatePart<StatePartNameType, any> } = {};
|
public statePartMap: { [key in StatePartNameType]?: StatePart<StatePartNameType, any> } = {};
|
||||||
|
|
||||||
|
private pendingStatePartCreation: Map<string, Promise<StatePart<StatePartNameType, any>>> = new Map();
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,8 +28,14 @@ export class Smartstate<StatePartNameType extends string> {
|
|||||||
initialArg?: PayloadType,
|
initialArg?: PayloadType,
|
||||||
initMode: TInitMode = 'soft'
|
initMode: TInitMode = 'soft'
|
||||||
): Promise<StatePart<StatePartNameType, PayloadType>> {
|
): Promise<StatePart<StatePartNameType, PayloadType>> {
|
||||||
|
// Return pending creation if one exists to prevent duplicate state parts
|
||||||
|
const pending = this.pendingStatePartCreation.get(statePartNameArg);
|
||||||
|
if (pending) {
|
||||||
|
return pending as Promise<StatePart<StatePartNameType, PayloadType>>;
|
||||||
|
}
|
||||||
|
|
||||||
const existingStatePart = this.statePartMap[statePartNameArg];
|
const existingStatePart = this.statePartMap[statePartNameArg];
|
||||||
|
|
||||||
if (existingStatePart) {
|
if (existingStatePart) {
|
||||||
switch (initMode) {
|
switch (initMode) {
|
||||||
case 'mandatory':
|
case 'mandatory':
|
||||||
@@ -36,7 +44,7 @@ export class Smartstate<StatePartNameType extends string> {
|
|||||||
);
|
);
|
||||||
case 'force':
|
case 'force':
|
||||||
// Force mode: create new state part
|
// Force mode: create new state part
|
||||||
return this.createStatePart<PayloadType>(statePartNameArg, initialArg, initMode);
|
break; // Fall through to creation
|
||||||
case 'soft':
|
case 'soft':
|
||||||
case 'persistent':
|
case 'persistent':
|
||||||
default:
|
default:
|
||||||
@@ -50,7 +58,16 @@ export class Smartstate<StatePartNameType extends string> {
|
|||||||
`State part '${statePartNameArg}' does not exist and no initial state provided`
|
`State part '${statePartNameArg}' does not exist and no initial state provided`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.createStatePart<PayloadType>(statePartNameArg, initialArg, initMode);
|
}
|
||||||
|
|
||||||
|
const creationPromise = this.createStatePart<PayloadType>(statePartNameArg, initialArg, initMode);
|
||||||
|
this.pendingStatePartCreation.set(statePartNameArg, creationPromise);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await creationPromise;
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
this.pendingStatePartCreation.delete(statePartNameArg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,10 +93,18 @@ export class Smartstate<StatePartNameType extends string> {
|
|||||||
);
|
);
|
||||||
await newState.init();
|
await newState.init();
|
||||||
const currentState = newState.getState();
|
const currentState = newState.getState();
|
||||||
await newState.setState({
|
|
||||||
...currentState,
|
if (initMode === 'persistent' && currentState !== undefined) {
|
||||||
...initialPayloadArg,
|
// Persisted state exists - merge with defaults, persisted values take precedence
|
||||||
});
|
await newState.setState({
|
||||||
|
...initialPayloadArg,
|
||||||
|
...currentState,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No persisted state or non-persistent mode
|
||||||
|
await newState.setState(initialPayloadArg);
|
||||||
|
}
|
||||||
|
|
||||||
this.statePartMap[statePartName] = newState;
|
this.statePartMap[statePartName] = newState;
|
||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|||||||
public stateStore: TStatePayload | undefined;
|
public stateStore: TStatePayload | undefined;
|
||||||
private cumulativeDeferred = plugins.smartpromise.cumulativeDefer();
|
private cumulativeDeferred = plugins.smartpromise.cumulativeDefer();
|
||||||
|
|
||||||
|
private pendingCumulativeNotification: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
private webStoreOptions: plugins.webstore.IWebStoreOptions;
|
private webStoreOptions: plugins.webstore.IWebStoreOptions;
|
||||||
private webStore: plugins.webstore.WebStore<TStatePayload> | null = null; // Add WebStore instance
|
private webStore: plugins.webstore.WebStore<TStatePayload> | null = null; // Add WebStore instance
|
||||||
|
|
||||||
@@ -50,14 +52,16 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|||||||
if (!this.validateState(newStateArg)) {
|
if (!this.validateState(newStateArg)) {
|
||||||
throw new Error(`Invalid state structure for state part '${this.name}'`);
|
throw new Error(`Invalid state structure for state part '${this.name}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.stateStore = newStateArg;
|
// Save to WebStore first to ensure atomicity - if save fails, memory state remains unchanged
|
||||||
await this.notifyChange();
|
|
||||||
|
|
||||||
// Save state to WebStore if initialized
|
|
||||||
if (this.webStore) {
|
if (this.webStore) {
|
||||||
await this.webStore.set(String(this.name), newStateArg);
|
await this.webStore.set(String(this.name), newStateArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update in-memory state after successful persistence
|
||||||
|
this.stateStore = newStateArg;
|
||||||
|
await this.notifyChange();
|
||||||
|
|
||||||
return this.stateStore;
|
return this.stateStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,8 +102,13 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|||||||
* creates a cumulative notification by adding a change notification at the end of the call stack;
|
* creates a cumulative notification by adding a change notification at the end of the call stack;
|
||||||
*/
|
*/
|
||||||
public notifyChangeCumulative() {
|
public notifyChangeCumulative() {
|
||||||
// TODO: check viability
|
// Debounce: clear any pending notification
|
||||||
setTimeout(async () => {
|
if (this.pendingCumulativeNotification) {
|
||||||
|
clearTimeout(this.pendingCumulativeNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingCumulativeNotification = setTimeout(async () => {
|
||||||
|
this.pendingCumulativeNotification = null;
|
||||||
if (this.stateStore) {
|
if (this.stateStore) {
|
||||||
await this.notifyChange();
|
await this.notifyChange();
|
||||||
}
|
}
|
||||||
@@ -122,7 +131,8 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|||||||
try {
|
try {
|
||||||
return selectorFn(stateArg);
|
return selectorFn(stateArg);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Nothing here
|
console.error(`Selector error in state part '${this.name}':`, e);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -151,20 +161,41 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|||||||
/**
|
/**
|
||||||
* waits until a certain part of the state becomes available
|
* waits until a certain part of the state becomes available
|
||||||
* @param selectorFn
|
* @param selectorFn
|
||||||
|
* @param timeoutMs - optional timeout in milliseconds to prevent indefinite waiting
|
||||||
*/
|
*/
|
||||||
public async waitUntilPresent<T = TStatePayload>(
|
public async waitUntilPresent<T = TStatePayload>(
|
||||||
selectorFn?: (state: TStatePayload) => T
|
selectorFn?: (state: TStatePayload) => T,
|
||||||
|
timeoutMs?: number
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const done = plugins.smartpromise.defer<T>();
|
const done = plugins.smartpromise.defer<T>();
|
||||||
const selectedObservable = this.select(selectorFn);
|
const selectedObservable = this.select(selectorFn);
|
||||||
const subscription = selectedObservable.subscribe(async (value) => {
|
let resolved = false;
|
||||||
if (value) {
|
|
||||||
|
const subscription = selectedObservable.subscribe((value) => {
|
||||||
|
if (value && !resolved) {
|
||||||
|
resolved = true;
|
||||||
done.resolve(value);
|
done.resolve(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const result = await done.promise;
|
|
||||||
subscription.unsubscribe();
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||||
return result;
|
if (timeoutMs) {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
subscription.unsubscribe();
|
||||||
|
done.reject(new Error(`waitUntilPresent timed out after ${timeoutMs}ms`));
|
||||||
|
}
|
||||||
|
}, timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await done.promise;
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -175,6 +206,6 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|||||||
) {
|
) {
|
||||||
const resultPromise = funcArg(this);
|
const resultPromise = funcArg(this);
|
||||||
this.cumulativeDeferred.addPromise(resultPromise);
|
this.cumulativeDeferred.addPromise(resultPromise);
|
||||||
this.setState(await resultPromise);
|
await this.setState(await resultPromise);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user