Add WebSocket support (#12)
This commit is contained in:
parent
3d27b7f313
commit
1aad0fce31
68 changed files with 2578 additions and 993 deletions
|
|
@ -1,93 +0,0 @@
|
|||
import { Logger } from "../tracing/logger";
|
||||
import type { RelativePath } from "../persistence/database";
|
||||
import { DocumentLocks } from "./document-locks";
|
||||
|
||||
describe("Document lock", () => {
|
||||
const testPath: RelativePath = "test/document/path";
|
||||
const logger = new Logger();
|
||||
let locks = new DocumentLocks(logger);
|
||||
|
||||
beforeEach(() => {
|
||||
locks = new DocumentLocks(logger);
|
||||
});
|
||||
|
||||
test("should lock a document successfully", () => {
|
||||
const result = locks.tryLockDocument(testPath);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should not lock a document that is already locked", () => {
|
||||
locks.tryLockDocument(testPath);
|
||||
const result = locks.tryLockDocument(testPath);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("should unlock a locked document", () => {
|
||||
locks.tryLockDocument(testPath);
|
||||
locks.unlockDocument(testPath);
|
||||
const result = locks.tryLockDocument(testPath);
|
||||
expect(result).toBe(true);
|
||||
locks.unlockDocument(testPath);
|
||||
});
|
||||
|
||||
test("should throw an error when unlocking a document that is not locked", () => {
|
||||
expect(() => {
|
||||
locks.unlockDocument(testPath);
|
||||
}).toThrow(`Document ${testPath} is not locked, cannot unlock`);
|
||||
});
|
||||
|
||||
test("should wait for a document lock and resolve when unlocked", async () => {
|
||||
locks.tryLockDocument(testPath);
|
||||
|
||||
let resolved = false;
|
||||
const waitPromise = locks.waitForDocumentLock(testPath).then(() => {
|
||||
resolved = true;
|
||||
});
|
||||
|
||||
locks.unlockDocument(testPath);
|
||||
await waitPromise;
|
||||
|
||||
expect(resolved).toBe(true);
|
||||
});
|
||||
|
||||
test("should resolve multiple waiters in FIFO order", async () => {
|
||||
locks.tryLockDocument(testPath);
|
||||
|
||||
let firstResolved = false;
|
||||
let secondResolved = false;
|
||||
let thirdResolved = false;
|
||||
|
||||
const firstWaitPromise = locks
|
||||
.waitForDocumentLock(testPath)
|
||||
.then(() => {
|
||||
firstResolved = true;
|
||||
});
|
||||
|
||||
const secondWaitPromise = locks
|
||||
.waitForDocumentLock(testPath)
|
||||
.then(() => {
|
||||
secondResolved = true;
|
||||
});
|
||||
|
||||
const thirdWaitPromise = locks
|
||||
.waitForDocumentLock(testPath)
|
||||
.then(() => {
|
||||
thirdResolved = true;
|
||||
});
|
||||
|
||||
locks.unlockDocument(testPath);
|
||||
await firstWaitPromise;
|
||||
expect(firstResolved).toBe(true);
|
||||
expect(secondResolved).toBe(false);
|
||||
expect(thirdResolved).toBe(false);
|
||||
|
||||
locks.unlockDocument(testPath);
|
||||
await secondWaitPromise;
|
||||
expect(secondResolved).toBe(true);
|
||||
expect(thirdResolved).toBe(false);
|
||||
|
||||
locks.unlockDocument(testPath);
|
||||
await thirdWaitPromise;
|
||||
expect(thirdResolved).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
import type { Logger } from "../tracing/logger";
|
||||
import type { RelativePath } from "../persistence/database";
|
||||
|
||||
// Manages locks on documents to prevent concurrent modifications
|
||||
// allowing the client's FileOperations implementation to be simpler.
|
||||
// Locks are granted in a first-in-first-out order.
|
||||
export class DocumentLocks {
|
||||
private readonly locked = new Set<RelativePath>();
|
||||
private readonly waiters = new Map<RelativePath, (() => void)[]>();
|
||||
|
||||
public constructor(private readonly logger: Logger) {}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
this.logger.debug(`Waiting for lock on ${relativePath}`);
|
||||
|
||||
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) {
|
||||
this.logger.debug(`Granted lock on ${relativePath}`);
|
||||
nextWaiting();
|
||||
} else {
|
||||
this.locked.delete(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.locked.clear();
|
||||
this.waiters.clear();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type { RelativePath } from "../persistence/database";
|
||||
import type { FileSystemOperations } from "./filesystem-operations";
|
||||
import type { Logger } from "../tracing/logger";
|
||||
import { DocumentLocks } from "./document-locks";
|
||||
import { Locks } from "../utils/locks";
|
||||
import { FileNotFoundError } from "./file-not-found-error";
|
||||
|
||||
/**
|
||||
|
|
@ -10,13 +10,13 @@ import { FileNotFoundError } from "./file-not-found-error";
|
|||
* single request in-flight for any one file through the use of locks.
|
||||
*/
|
||||
export class SafeFileSystemOperations implements FileSystemOperations {
|
||||
private readonly locks: DocumentLocks;
|
||||
private readonly locks: Locks<RelativePath>;
|
||||
|
||||
public constructor(
|
||||
private readonly fs: FileSystemOperations,
|
||||
private readonly logger: Logger
|
||||
) {
|
||||
this.locks = new DocumentLocks(logger);
|
||||
this.locks = new Locks(logger);
|
||||
}
|
||||
|
||||
public async listAllFiles(): Promise<RelativePath[]> {
|
||||
|
|
@ -117,7 +117,7 @@ export class SafeFileSystemOperations implements FileSystemOperations {
|
|||
: [pathOrPaths];
|
||||
|
||||
await Promise.all(
|
||||
paths.map(async (path) => this.locks.waitForDocumentLock(path))
|
||||
paths.map(async (path) => this.locks.waitForLock(path))
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
@ -125,7 +125,7 @@ export class SafeFileSystemOperations implements FileSystemOperations {
|
|||
} finally {
|
||||
await Promise.all(
|
||||
paths.map((path) => {
|
||||
this.locks.unlockDocument(path);
|
||||
this.locks.unlock(path);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue