fix(smartstate): prevent duplicate statepart creation and fix persistence/notification race conditions

This commit is contained in:
2026-02-02 01:05:57 +00:00
parent eb1c48bee4
commit 6436370abc
4 changed files with 91 additions and 23 deletions

View File

@@ -7,6 +7,8 @@ export class StatePart<TStatePartName, TStatePayload> {
public stateStore: TStatePayload | undefined;
private cumulativeDeferred = plugins.smartpromise.cumulativeDefer();
private pendingCumulativeNotification: ReturnType<typeof setTimeout> | null = null;
private webStoreOptions: plugins.webstore.IWebStoreOptions;
private webStore: plugins.webstore.WebStore<TStatePayload> | null = null; // Add WebStore instance
@@ -50,14 +52,16 @@ export class StatePart<TStatePartName, TStatePayload> {
if (!this.validateState(newStateArg)) {
throw new Error(`Invalid state structure for state part '${this.name}'`);
}
this.stateStore = newStateArg;
await this.notifyChange();
// Save state to WebStore if initialized
// Save to WebStore first to ensure atomicity - if save fails, memory state remains unchanged
if (this.webStore) {
await this.webStore.set(String(this.name), newStateArg);
}
// Update in-memory state after successful persistence
this.stateStore = newStateArg;
await this.notifyChange();
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;
*/
public notifyChangeCumulative() {
// TODO: check viability
setTimeout(async () => {
// Debounce: clear any pending notification
if (this.pendingCumulativeNotification) {
clearTimeout(this.pendingCumulativeNotification);
}
this.pendingCumulativeNotification = setTimeout(async () => {
this.pendingCumulativeNotification = null;
if (this.stateStore) {
await this.notifyChange();
}
@@ -122,7 +131,8 @@ export class StatePart<TStatePartName, TStatePayload> {
try {
return selectorFn(stateArg);
} 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
* @param selectorFn
* @param timeoutMs - optional timeout in milliseconds to prevent indefinite waiting
*/
public async waitUntilPresent<T = TStatePayload>(
selectorFn?: (state: TStatePayload) => T
selectorFn?: (state: TStatePayload) => T,
timeoutMs?: number
): Promise<T> {
const done = plugins.smartpromise.defer<T>();
const selectedObservable = this.select(selectorFn);
const subscription = selectedObservable.subscribe(async (value) => {
if (value) {
let resolved = false;
const subscription = selectedObservable.subscribe((value) => {
if (value && !resolved) {
resolved = true;
done.resolve(value);
}
});
const result = await done.promise;
subscription.unsubscribe();
return result;
let timeoutId: ReturnType<typeof setTimeout> | undefined;
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);
this.cumulativeDeferred.addPromise(resultPromise);
this.setState(await resultPromise);
await this.setState(await resultPromise);
}
}