All files Locks.ts

91.11% Statements 41/45
68.96% Branches 20/29
87.5% Functions 7/8
90.9% Lines 40/44

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80  1x 1x 1x                                     9x 9x 9x     33x   33x 33x 33x 33x 33x 17x 16x 16x 16x 12x 11x   16x     7x 7x 7x 1x 6x 6x 6x     9x 9x   9x 20x 20x 8x 12x 12x 1x   8x 8x     8x         1x 1x          
const defaultStore =
  typeof window === 'object' && window && typeof window.localStorage === 'object' ? window.localStorage : null;
 
let _locks: Locks | undefined;
 
/**
 * Creates a lock manager, which can create exclusive locks across browser tabs.
 * Uses `window.localStorage` by default to lock across tabs.
 *
 * Below example, will wait for 5 seconds to acquire a lock, and then execute
 * the function once lock is acquired and release the lock after function
 * execution. It will fail with `LOCK_TIMEOUT` error if lock is not acquired
 * within the 5 seconds. The lock will acquired for 2 seconds (default 1000ms).
 *
 * ```ts
 * Locks.get().lock('my-lock', 2000, 5000)(async () => {
 *   console.log('Lock acquired');
 * });
 * ```
 */
export class Locks {
  public static get = (): Locks => {
    if (!_locks) _locks = new Locks();
    return _locks;
  };
 
  constructor(
    protIected readonly store: Record<string, string> = defaultStore || {},
    protected readonly now = Date.now,
    protected readonly pfx = 'lock-',
  ) {}
 
  public acquire(id: string, ms = 1000): (() => void) | undefined {
    if (ms <= 0) return;
    const key = this.pfx + id;
    const lockUntil = this.store[key];
    const now = this.now();
    const isLocked = lockUntil !== undefined && parseInt(lockUntil, 36) > now;
    if (isLocked) return;
    const lockUntilNex = (now + ms).toString(36);
    this.store[key] = lockUntilNex;
    const unlock = () => {
      if (this.store[key] === lockUntilNex) delete this.store[key];
    };
    return unlock;
  }
 
  public isLocked(id: string): boolean {
    const key = this.pfx + id;
    const lockUntil = this.store[key];
    if (lockUntil === undefined) return false;
    const now = this.now();
    const lockUntilNum = parseInt(lockUntil, 36);
    return lockUntilNum > now;
  }
 
  public lock(
    id: string,
    ms?: number,
    timeoutMs: number = 2 * 1000,
    checkMs: number = 10,
  ): <T>(fn: () => Promise<T>) => Promise<T> {
    return async <T>(fn: () => Promise<T>): Promise<T> => {
      const timeout = this.now() + timeoutMs;
      let unlock: (() => void) | undefined;
      while (!unlock) {
        unlock = this.acquire(id, ms);
        if (unlock) break;
        await new Promise((r) => setTimeout(r, checkMs));
        if (this.now() > timeout) throw new Error('LOCK_TIMEOUT');
      }
      try {
        return await fn();
      } finally {
        unlock!();
      }
    };
  }
}