Stop using global file locks
This commit is contained in:
parent
f8dcd33d3e
commit
3d8067e947
3 changed files with 89 additions and 86 deletions
|
|
@ -1,48 +0,0 @@
|
||||||
import type { RelativePath } from "../persistence/database";
|
|
||||||
|
|
||||||
const locked = new Set<RelativePath>();
|
|
||||||
const waiters = new Map<RelativePath, (() => void)[]>();
|
|
||||||
|
|
||||||
export function tryLockDocument(relativePath: RelativePath): boolean {
|
|
||||||
if (locked.has(relativePath)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
locked.add(relativePath);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function waitForDocumentLock(
|
|
||||||
relativePath: RelativePath
|
|
||||||
): Promise<void> {
|
|
||||||
if (tryLockDocument(relativePath)) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
let waiting = waiters.get(relativePath);
|
|
||||||
if (!waiting) {
|
|
||||||
waiting = [];
|
|
||||||
waiters.set(relativePath, waiting);
|
|
||||||
}
|
|
||||||
|
|
||||||
waiting.push(resolve);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unlockDocument(relativePath: RelativePath): void {
|
|
||||||
if (!locked.has(relativePath)) {
|
|
||||||
throw new Error(
|
|
||||||
`Document ${relativePath} is not locked, cannot unlock`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the first element to ensure FIFO unblocking order
|
|
||||||
const nextWaiting = waiters.get(relativePath)?.shift();
|
|
||||||
|
|
||||||
if (nextWaiting) {
|
|
||||||
nextWaiting();
|
|
||||||
} else {
|
|
||||||
locked.delete(relativePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,89 +1,90 @@
|
||||||
import { RelativePath } from "../persistence/database";
|
import type { RelativePath } from "../persistence/database";
|
||||||
import {
|
import { DocumentLocks } from "./document-locks";
|
||||||
tryLockDocument,
|
|
||||||
waitForDocumentLock,
|
|
||||||
unlockDocument
|
|
||||||
} from "./document-lock";
|
|
||||||
|
|
||||||
describe("Document Lock Operations", () => {
|
describe("Document lock", () => {
|
||||||
const testPath: RelativePath = "test/document/path";
|
const testPath: RelativePath = "test/document/path";
|
||||||
|
let locks = new DocumentLocks();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Reset the state before each test
|
locks = new DocumentLocks();
|
||||||
(global as any).locked = new Set<RelativePath>();
|
|
||||||
(global as any).waiters = new Map<RelativePath, (() => void)[]>();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should lock a document successfully", () => {
|
test("should lock a document successfully", () => {
|
||||||
const result = tryLockDocument(testPath);
|
const result = locks.tryLockDocument(testPath);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should not lock a document that is already locked", () => {
|
test("should not lock a document that is already locked", () => {
|
||||||
tryLockDocument(testPath);
|
locks.tryLockDocument(testPath);
|
||||||
const result = tryLockDocument(testPath);
|
const result = locks.tryLockDocument(testPath);
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should unlock a locked document", () => {
|
test("should unlock a locked document", () => {
|
||||||
tryLockDocument(testPath);
|
locks.tryLockDocument(testPath);
|
||||||
unlockDocument(testPath);
|
locks.unlockDocument(testPath);
|
||||||
const result = tryLockDocument(testPath);
|
const result = locks.tryLockDocument(testPath);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
unlockDocument(testPath);
|
locks.unlockDocument(testPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw an error when unlocking a document that is not locked", () => {
|
test("should throw an error when unlocking a document that is not locked", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
unlockDocument(testPath);
|
locks.unlockDocument(testPath);
|
||||||
}).toThrow(`Document ${testPath} is not locked, cannot unlock`);
|
}).toThrow(`Document ${testPath} is not locked, cannot unlock`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should wait for a document lock and resolve when unlocked", async () => {
|
test("should wait for a document lock and resolve when unlocked", async () => {
|
||||||
tryLockDocument(testPath);
|
locks.tryLockDocument(testPath);
|
||||||
|
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
const waitPromise = waitForDocumentLock(testPath).then(() => {
|
const waitPromise = locks.waitForDocumentLock(testPath).then(() => {
|
||||||
resolved = true;
|
resolved = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
unlockDocument(testPath);
|
locks.unlockDocument(testPath);
|
||||||
await waitPromise;
|
await waitPromise;
|
||||||
|
|
||||||
expect(resolved).toBe(true);
|
expect(resolved).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should resolve multiple waiters in FIFO order", async () => {
|
test("should resolve multiple waiters in FIFO order", async () => {
|
||||||
tryLockDocument(testPath);
|
locks.tryLockDocument(testPath);
|
||||||
|
|
||||||
let firstResolved = false;
|
let firstResolved = false;
|
||||||
let secondResolved = false;
|
let secondResolved = false;
|
||||||
let thirdResolved = false;
|
let thirdResolved = false;
|
||||||
|
|
||||||
const firstWaitPromise = waitForDocumentLock(testPath).then(() => {
|
const firstWaitPromise = locks
|
||||||
|
.waitForDocumentLock(testPath)
|
||||||
|
.then(() => {
|
||||||
firstResolved = true;
|
firstResolved = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const secondWaitPromise = waitForDocumentLock(testPath).then(() => {
|
const secondWaitPromise = locks
|
||||||
|
.waitForDocumentLock(testPath)
|
||||||
|
.then(() => {
|
||||||
secondResolved = true;
|
secondResolved = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const thirdWaitPromise = waitForDocumentLock(testPath).then(() => {
|
const thirdWaitPromise = locks
|
||||||
|
.waitForDocumentLock(testPath)
|
||||||
|
.then(() => {
|
||||||
thirdResolved = true;
|
thirdResolved = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
unlockDocument(testPath);
|
locks.unlockDocument(testPath);
|
||||||
await firstWaitPromise;
|
await firstWaitPromise;
|
||||||
expect(firstResolved).toBe(true);
|
expect(firstResolved).toBe(true);
|
||||||
expect(secondResolved).toBe(false);
|
expect(secondResolved).toBe(false);
|
||||||
expect(thirdResolved).toBe(false);
|
expect(thirdResolved).toBe(false);
|
||||||
|
|
||||||
unlockDocument(testPath);
|
locks.unlockDocument(testPath);
|
||||||
await secondWaitPromise;
|
await secondWaitPromise;
|
||||||
expect(secondResolved).toBe(true);
|
expect(secondResolved).toBe(true);
|
||||||
expect(thirdResolved).toBe(false);
|
expect(thirdResolved).toBe(false);
|
||||||
|
|
||||||
unlockDocument(testPath);
|
locks.unlockDocument(testPath);
|
||||||
await thirdWaitPromise;
|
await thirdWaitPromise;
|
||||||
expect(thirdResolved).toBe(true);
|
expect(thirdResolved).toBe(true);
|
||||||
});
|
});
|
||||||
50
frontend/sync-client/src/sync-operations/document-locks.ts
Normal file
50
frontend/sync-client/src/sync-operations/document-locks.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import type { RelativePath } from "../persistence/database";
|
||||||
|
|
||||||
|
export class DocumentLocks {
|
||||||
|
private readonly locked = new Set<RelativePath>();
|
||||||
|
private readonly waiters = new Map<RelativePath, (() => void)[]>();
|
||||||
|
|
||||||
|
public tryLockDocument(relativePath: RelativePath): boolean {
|
||||||
|
if (this.locked.has(relativePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.locked.add(relativePath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async waitForDocumentLock(
|
||||||
|
relativePath: RelativePath
|
||||||
|
): Promise<void> {
|
||||||
|
if (this.tryLockDocument(relativePath)) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let waiting = this.waiters.get(relativePath);
|
||||||
|
if (!waiting) {
|
||||||
|
waiting = [];
|
||||||
|
this.waiters.set(relativePath, waiting);
|
||||||
|
}
|
||||||
|
|
||||||
|
waiting.push(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public unlockDocument(relativePath: RelativePath): void {
|
||||||
|
if (!this.locked.has(relativePath)) {
|
||||||
|
throw new Error(
|
||||||
|
`Document ${relativePath} is not locked, cannot unlock`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the first element to ensure FIFO unblocking order
|
||||||
|
const nextWaiting = this.waiters.get(relativePath)?.shift();
|
||||||
|
|
||||||
|
if (nextWaiting) {
|
||||||
|
nextWaiting();
|
||||||
|
} else {
|
||||||
|
this.locked.delete(relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue