diff --git a/frontend/sync-client/src/utils/data-structures/locks.ts b/frontend/sync-client/src/utils/data-structures/locks.ts index 6a801e12..e835a4a3 100644 --- a/frontend/sync-client/src/utils/data-structures/locks.ts +++ b/frontend/sync-client/src/utils/data-structures/locks.ts @@ -49,14 +49,17 @@ export class Locks { fn: () => R | Promise ): Promise { const keys = Array.isArray(keyOrKeys) ? keyOrKeys : [keyOrKeys]; - keys.sort((a, b) => String(a).localeCompare(String(b))); // Ensure consistent order to prevent deadlocks - await Promise.all(keys.map(async (key) => this.waitForLock(key))); + // Deduplicate keys to prevent deadlock from acquiring same lock twice + const uniqueKeys = Array.from(new Set(keys)); + uniqueKeys.sort((a, b) => String(a).localeCompare(String(b))); // Ensure consistent order to prevent deadlocks + + await Promise.all(uniqueKeys.map(async (key) => this.waitForLock(key))); try { return await fn(); } finally { - keys.forEach((key) => { + uniqueKeys.forEach((key) => { this.unlock(key); }); } diff --git a/frontend/sync-client/src/utils/data-structures/min-covered.test.ts b/frontend/sync-client/src/utils/data-structures/min-covered.test.ts index 82f792c3..1bbd1425 100644 --- a/frontend/sync-client/src/utils/data-structures/min-covered.test.ts +++ b/frontend/sync-client/src/utils/data-structures/min-covered.test.ts @@ -48,15 +48,29 @@ describe("CoveredValues", () => { assert.strictEqual(covered.min, 6); }); - it("should handle force setting min value", () => { + it("should auto-advance when setting min value", () => { const covered = new CoveredValues(5); covered.add(7); covered.add(8); covered.add(9); assert.strictEqual(covered.min, 5); + // Setting min to 6 should auto-advance through 7, 8, 9 covered.min = 6; - assert.strictEqual(covered.min, 6); + assert.strictEqual(covered.min, 9); covered.add(10); assert.strictEqual(covered.min, 10); }); + + it("should handle setting min value with no consecutive values", () => { + const covered = new CoveredValues(5); + covered.add(10); + covered.add(15); + assert.strictEqual(covered.min, 5); + // Setting min to 8 should not auto-advance (no consecutive values) + covered.min = 8; + assert.strictEqual(covered.min, 8); + // Add 9 to trigger auto-advance to 10 + covered.add(9); + assert.strictEqual(covered.min, 10); + }); }); diff --git a/frontend/sync-client/src/utils/data-structures/min-covered.ts b/frontend/sync-client/src/utils/data-structures/min-covered.ts index c453ef88..d55746df 100644 --- a/frontend/sync-client/src/utils/data-structures/min-covered.ts +++ b/frontend/sync-client/src/utils/data-structures/min-covered.ts @@ -24,7 +24,8 @@ export class CoveredValues { public set min(value: number) { this.minValue = Math.max(value, this.minValue); - this.seenValues = this.seenValues.filter((v) => v > value); + this.seenValues = this.seenValues.filter((v) => v > this.minValue); + this.advanceMinWhilePossible(); } public add(value: number): void { @@ -45,6 +46,10 @@ export class CoveredValues { this.seenValues.splice(i, 0, value); } + this.advanceMinWhilePossible(); + } + + private advanceMinWhilePossible(): void { while ( this.seenValues.length > 0 && this.seenValues[0] === this.minValue + 1