Expose locks utils

This commit is contained in:
Andras Schmelczer 2025-08-10 12:59:33 +01:00
parent b56e8f6c15
commit d9ffcfeb5c
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
2 changed files with 42 additions and 5 deletions

View file

@ -20,3 +20,8 @@ export type { ClientCursors } from "./services/types/ClientCursors";
export type { NetworkConnectionStatus } from "./types/network-connection-status";
export { DocumentUpdateStatus } from "./types/document-update-status";
export { SyncClient } from "./sync-client";
import { Locks } from "./utils/locks";
export const helpers = {
Locks
};

View file

@ -1,14 +1,27 @@
import type { Logger } from "../tracing/logger";
// Manages locks on T to prevent concurrent modifications
// allowing the client's FileOperations implementation to be simpler.
// Locks are granted in a first-in-first-out order.
/**
* Manages exclusive locks on items to prevent concurrent modifications.
* Locks are granted in FIFO order.
*
* @template T The type of the key used for locking
*/
export class Locks<T> {
/** Currently locked keys */
private readonly locked = new Set<T>();
/** Queue of resolve functions waiting for each key */
private readonly waiters = new Map<T, (() => unknown)[]>();
public constructor(private readonly logger: Logger) {}
/**
* Attempts to acquire a lock immediately without waiting.
* Must call `unlock()` if successful.
*
* @param key The key to lock
* @returns `true` if lock acquired, `false` if already locked
*/
public tryLock(key: T): boolean {
if (this.locked.has(key)) {
return false;
@ -19,6 +32,13 @@ export class Locks<T> {
return true;
}
/**
* Waits to acquire a lock, blocking until available.
* Operations are queued in FIFO order. Must call `unlock()` when done.
*
* @param key The key to wait for and lock
* @returns Promise that resolves when lock is acquired
*/
public async waitForLock(key: T): Promise<void> {
if (this.tryLock(key)) {
return Promise.resolve();
@ -27,6 +47,7 @@ export class Locks<T> {
this.logger.debug(`Waiting for lock on ${key}`);
return new Promise((resolve) => {
// DefaultDict behavior
let waiting = this.waiters.get(key);
if (!waiting) {
waiting = [];
@ -37,12 +58,19 @@ export class Locks<T> {
});
}
/**
* Releases a lock and grants access to the next waiting operation in FIFO order.
* Removes the key from locked set if no waiters.
*
* @param key The key to unlock
* @throws {Error} If key is not currently locked
*/
public unlock(key: T): void {
if (!this.locked.has(key)) {
throw new Error(`Document ${key} is not locked, cannot unlock`);
throw new Error(`Key ${key} is not locked, cannot unlock`);
}
// Remove the first element to ensure FIFO unblocking order
// Remove first waiter to ensure FIFO order
const nextWaiting = this.waiters.get(key)?.shift();
if (nextWaiting) {
@ -53,6 +81,10 @@ export class Locks<T> {
}
}
/**
* Clears all locks and waiters. Causes waiting operations to hang indefinitely.
* Use with caution.
*/
public reset(): void {
this.locked.clear();
this.waiters.clear();