From d9ffcfeb5c607f821b78fdc4eb95d603a12ba1fb Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 10 Aug 2025 12:59:33 +0100 Subject: [PATCH] Expose locks utils --- frontend/sync-client/src/index.ts | 5 +++ frontend/sync-client/src/utils/locks.ts | 42 ++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/frontend/sync-client/src/index.ts b/frontend/sync-client/src/index.ts index e984794d..4a5f5d1e 100644 --- a/frontend/sync-client/src/index.ts +++ b/frontend/sync-client/src/index.ts @@ -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 +}; diff --git a/frontend/sync-client/src/utils/locks.ts b/frontend/sync-client/src/utils/locks.ts index 7e75bd3d..8e52cba0 100644 --- a/frontend/sync-client/src/utils/locks.ts +++ b/frontend/sync-client/src/utils/locks.ts @@ -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 { + /** Currently locked keys */ private readonly locked = new Set(); + + /** Queue of resolve functions waiting for each key */ private readonly waiters = new Map 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 { 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 { if (this.tryLock(key)) { return Promise.resolve(); @@ -27,6 +47,7 @@ export class Locks { 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 { }); } + /** + * 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 { } } + /** + * Clears all locks and waiters. Causes waiting operations to hang indefinitely. + * Use with caution. + */ public reset(): void { this.locked.clear(); this.waiters.clear();