Skip to content

Commit a70a2eb

Browse files
heskewclaude
andcommitted
address review: delegate return()/throw() in endIteratorOnCorruptFrame
Both PR review bots flagged that the corrupt-frame iterator wrapper dropped the underlying iterator's optional return()/throw(), so early termination (a for-of break or an outer .return()) would skip the source iterator's cleanup. The current rocksdb-js query iterator implements neither, so nothing leaks today, but the wrapper should stay a faithful proxy — delegate both when present (guarded by typeof), and latch stopped on return(). Adds unit tests for delegation and for the no-synthesis-when-absent case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e452758 commit a70a2eb

2 files changed

Lines changed: 58 additions & 1 deletion

File tree

resources/replayLogsGuards.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function endIteratorOnCorruptFrame<T>(
6363
onCorruptFrame: (error: RangeError) => void
6464
): IterableIterator<T> {
6565
let stopped = false;
66-
return {
66+
const wrapped: IterableIterator<T> = {
6767
[Symbol.iterator]() {
6868
return this;
6969
},
@@ -85,4 +85,18 @@ export function endIteratorOnCorruptFrame<T>(
8585
}
8686
},
8787
};
88+
// Stay a faithful proxy: delegate the optional return()/throw() so early termination
89+
// (a for-of break, or an outer .return()) still releases whatever the source iterator
90+
// holds. The current rocksdb-js query iterator implements neither, but a future one that
91+
// adds cleanup must not be silently bypassed by this wrapper.
92+
if (iterator.return) {
93+
wrapped.return = (value?: any): IteratorResult<T> => {
94+
stopped = true;
95+
return iterator.return!(value);
96+
};
97+
}
98+
if (iterator.throw) {
99+
wrapped.throw = (error?: any): IteratorResult<T> => iterator.throw!(error);
100+
}
101+
return wrapped;
88102
}

unitTests/resources/replayLogs.test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,47 @@ describe('endIteratorOnCorruptFrame', () => {
116116
assert.deepStrictEqual([...wrapped], [1]);
117117
assert.strictEqual(reported, 0);
118118
});
119+
120+
it('delegates return()/throw() to the underlying iterator so early-exit cleanup runs', () => {
121+
let returnedWith;
122+
let threwWith;
123+
const source = {
124+
next() {
125+
return { done: false, value: 1 };
126+
},
127+
return(value) {
128+
returnedWith = value;
129+
return { done: true, value };
130+
},
131+
throw(error) {
132+
threwWith = error;
133+
return { done: true, value: undefined };
134+
},
135+
};
136+
const wrapped = endIteratorOnCorruptFrame(source, () => {});
137+
138+
assert.strictEqual(typeof wrapped.return, 'function');
139+
assert.deepStrictEqual(wrapped.return('cleanup'), { done: true, value: 'cleanup' });
140+
assert.strictEqual(returnedWith, 'cleanup');
141+
// after return(), the wrapper is latched done and never touches the source again
142+
assert.deepStrictEqual(wrapped.next(), { done: true, value: undefined });
143+
144+
assert.strictEqual(typeof wrapped.throw, 'function');
145+
const boom = new Error('boom');
146+
wrapped.throw(boom);
147+
assert.strictEqual(threwWith, boom);
148+
});
149+
150+
it('does not synthesize return()/throw() when the underlying iterator lacks them', () => {
151+
const wrapped = endIteratorOnCorruptFrame(
152+
{
153+
next() {
154+
return { done: true, value: undefined };
155+
},
156+
},
157+
() => {}
158+
);
159+
assert.strictEqual(wrapped.return, undefined);
160+
assert.strictEqual(wrapped.throw, undefined);
161+
});
119162
});

0 commit comments

Comments
 (0)