Skip to content

Commit 929bb42

Browse files
pubkeyclaude
andauthored
Fix RxState write queue permanently breaking after a modifier throws (#8398)
When a user-supplied modifier passed to RxState.set() throws, the shared write queue promise became permanently rejected, causing all subsequent set() calls to reject with an unrelated SNH error instead of performing the write. The fix splits the per-call write promise from the shared queue promise so a failing write still propagates its error to the caller but does not block future writes. Co-authored-by: Claude <noreply@anthropic.com>
1 parent de5301b commit 929bb42

3 files changed

Lines changed: 37 additions & 8 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- FIX `RxState.set()` permanently breaking the write queue when a user-supplied modifier throws, causing all subsequent `set()` calls to reject with an unrelated `SNH` error instead of performing the write

src/plugins/state/rx-state.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ import {
4242
RxStateOperation,
4343
RxStateModifier
4444
} from './types.ts';
45-
import { newRxError } from '../../rx-error.ts';
4645
import { runPluginHooks } from '../../hooks.ts';
4746

4847

@@ -144,7 +143,7 @@ export class RxStateBase<T, Reactivity = unknown> {
144143
* that would throw conflict errors and trigger a retry.
145144
*/
146145
_triggerWrite() {
147-
this._writeQueue = this._writeQueue.then(async () => {
146+
const next = this._writeQueue.then(async () => {
148147
if (this._nonPersisted.length === 0) {
149148
return;
150149
}
@@ -207,13 +206,10 @@ export class RxStateBase<T, Reactivity = unknown> {
207206
await promiseWait(0);
208207
}
209208
}
210-
}).catch(error => {
211-
throw newRxError('SNH', {
212-
name: 'RxState WRITE QUEUE ERROR',
213-
error
214-
});
215209
});
216-
return this._writeQueue;
210+
// Keep the shared queue alive so a failing write does not block subsequent ones.
211+
this._writeQueue = next.catch(() => { });
212+
return next;
217213
}
218214

219215
mergeOperationsIntoState(

test/unit/rx-state.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,38 @@ addRxPlugin(RxDBJsonDumpPlugin);
743743
// State must still be correct
744744
assert.strictEqual(state.a, 6);
745745

746+
state.collection.database.remove();
747+
});
748+
/**
749+
* When a modifier passed to set() throws, the write queue
750+
* must not be permanently broken. Subsequent writes with
751+
* valid modifiers should still succeed.
752+
*/
753+
it('write queue should recover after a modifier throws', async () => {
754+
const state = await getState();
755+
756+
await state.set('a', () => 1);
757+
assert.strictEqual(state.get('a'), 1);
758+
759+
const thrownError = new Error('bad modifier');
760+
let caughtError: any;
761+
try {
762+
await state.set('a', () => {
763+
throw thrownError;
764+
});
765+
} catch (err) {
766+
caughtError = err;
767+
}
768+
assert.ok(caughtError, 'the first set() should reject');
769+
770+
// a subsequent valid write must still succeed
771+
await state.set('a', () => 2);
772+
assert.strictEqual(state.get('a'), 2);
773+
774+
// and a later write should still work
775+
await state.set('a', (prev: any) => prev + 1);
776+
assert.strictEqual(state.get('a'), 3);
777+
746778
state.collection.database.remove();
747779
});
748780
});

0 commit comments

Comments
 (0)