fix(core): serialize state mutations, fix batch flushing/reentrancy, handle falsy initial values, dispose old StatePart on force, and improve notification/error handling

This commit is contained in:
2026-02-28 08:52:41 +00:00
parent 2f0b39ae41
commit 9312b8908c
8 changed files with 383 additions and 120 deletions

View File

@@ -34,8 +34,8 @@ export class StatePart<TStatePartName, TStatePayload> {
public smartstateRef?: Smartstate<any>;
private cumulativeDeferred = plugins.smartpromise.cumulativeDefer();
private mutationQueue: Promise<any> = Promise.resolve();
private pendingCumulativeNotification: ReturnType<typeof setTimeout> | null = null;
private pendingBatchNotification = false;
private webStoreOptions: plugins.webstore.IWebStoreOptions;
private webStore: plugins.webstore.WebStore<TStatePayload> | null = null;
@@ -92,9 +92,19 @@ export class StatePart<TStatePartName, TStatePayload> {
}
/**
* sets the stateStore to the new state
* sets the stateStore to the new state (serialized via mutation queue)
*/
public async setState(newStateArg: TStatePayload) {
public async setState(newStateArg: TStatePayload): Promise<TStatePayload> {
return this.mutationQueue = this.mutationQueue.then(
() => this.applyState(newStateArg),
() => this.applyState(newStateArg),
);
}
/**
* applies the state change (middleware → validate → persist → notify)
*/
private async applyState(newStateArg: TStatePayload): Promise<TStatePayload> {
// Run middleware chain
let processedState = newStateArg;
for (const mw of this.middlewares) {
@@ -129,13 +139,13 @@ export class StatePart<TStatePartName, TStatePayload> {
* notifies of a change on the state
*/
public async notifyChange() {
if (!this.stateStore) {
const snapshot = this.stateStore;
if (snapshot === undefined) {
return;
}
// If inside a batch, defer the notification
if (this.smartstateRef?.isBatching) {
this.pendingBatchNotification = true;
this.smartstateRef.registerPendingNotification(this);
return;
}
@@ -143,16 +153,19 @@ export class StatePart<TStatePartName, TStatePayload> {
const createStateHash = async (stateArg: any) => {
return await plugins.smarthashWeb.sha256FromString(plugins.smartjson.stableOneWayStringify(stateArg));
};
const currentHash = await createStateHash(this.stateStore);
if (
this.lastStateNotificationPayloadHash &&
currentHash === this.lastStateNotificationPayloadHash
) {
return;
} else {
try {
const currentHash = await createStateHash(snapshot);
if (
this.lastStateNotificationPayloadHash &&
currentHash === this.lastStateNotificationPayloadHash
) {
return;
}
this.lastStateNotificationPayloadHash = currentHash;
} catch (err) {
console.error(`State hash computation failed for '${this.name}':`, err);
}
this.state.next(this.stateStore);
this.state.next(snapshot);
}
private lastStateNotificationPayloadHash: any;
@@ -164,10 +177,12 @@ export class StatePart<TStatePartName, TStatePayload> {
clearTimeout(this.pendingCumulativeNotification);
}
this.pendingCumulativeNotification = setTimeout(async () => {
this.pendingCumulativeNotification = setTimeout(() => {
this.pendingCumulativeNotification = null;
if (this.stateStore) {
await this.notifyChange();
if (this.stateStore !== undefined) {
this.notifyChange().catch((err) => {
console.error(`notifyChangeCumulative failed for '${this.name}':`, err);
});
}
}, 0);
}
@@ -239,9 +254,16 @@ export class StatePart<TStatePartName, TStatePayload> {
*/
public async dispatchAction<T>(stateAction: StateAction<TStatePayload, T>, actionPayload: T): Promise<TStatePayload> {
await this.cumulativeDeferred.promise;
const newState = await stateAction.actionDef(this, actionPayload);
await this.setState(newState);
return this.getState();
return this.mutationQueue = this.mutationQueue.then(
async () => {
const newState = await stateAction.actionDef(this, actionPayload);
return this.applyState(newState);
},
async () => {
const newState = await stateAction.actionDef(this, actionPayload);
return this.applyState(newState);
},
);
}
/**
@@ -272,7 +294,7 @@ export class StatePart<TStatePartName, TStatePayload> {
}
const subscription = selectedObservable.subscribe((value) => {
if (value && !resolved) {
if (value !== undefined && value !== null && !resolved) {
resolved = true;
done.resolve(value);
}
@@ -325,4 +347,20 @@ export class StatePart<TStatePartName, TStatePayload> {
this.cumulativeDeferred.addPromise(resultPromise);
await this.setState(await resultPromise);
}
/**
* disposes the state part, completing the Subject and cleaning up resources
*/
public dispose(): void {
this.state.complete();
if (this.pendingCumulativeNotification) {
clearTimeout(this.pendingCumulativeNotification);
this.pendingCumulativeNotification = null;
}
this.middlewares.length = 0;
this.selectorCache = new WeakMap();
this.defaultSelectObservable = null;
this.webStore = null;
this.smartstateRef = undefined;
}
}