From d41d2cd969c6729daefbb21daf6b079142562bf5 Mon Sep 17 00:00:00 2001 From: Stojan Dimitrovski Date: Tue, 29 Oct 2024 18:00:40 +0100 Subject: [PATCH] feat: add process lock for optional use in non-browser environments (React Native) --- src/lib/locks.ts | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/lib/locks.ts b/src/lib/locks.ts index e29ec27bb..a9673d151 100644 --- a/src/lib/locks.ts +++ b/src/lib/locks.ts @@ -29,6 +29,7 @@ export abstract class LockAcquireTimeoutError extends Error { } export class NavigatorLockAcquireTimeoutError extends LockAcquireTimeoutError {} +export class ProcessLockAcquireTimeoutError extends LockAcquireTimeoutError {} /** * Implements a global exclusive lock using the Navigator LockManager API. It @@ -141,3 +142,63 @@ export async function navigatorLock( } ) } + +const PROCESS_LOCKS: { [name: string]: Promise } = {} + +/** + * Implements a global exclusive lock that works only in the current process. + * Useful for environments like React Native or other non-browser + * single-process (i.e. no concept of "tabs") environments. + * + * Use {@link #navigatorLock} in browser environments. + * + * @param name Name of the lock to be acquired. + * @param acquireTimeout If negative, no timeout. If 0 an error is thrown if + * the lock can't be acquired without waiting. If positive, the lock acquire + * will time out after so many milliseconds. An error is + * a timeout if it has `isAcquireTimeout` set to true. + * @param fn The operation to run once the lock is acquired. + */ +export async function processLock( + name: string, + acquireTimeout: number, + fn: () => Promise +): Promise { + const currentOperation = Promise.race( + [ + (PROCESS_LOCKS[name] ?? Promise.resolve()).catch((e: any) => { + // ignore error of previous operation that we're waiting to finish + return null + }), + acquireTimeout >= 0 + ? new Promise((_, reject) => { + setTimeout(() => { + reject( + new ProcessLockAcquireTimeoutError( + `Acquring process lock with name "${name}" timed out` + ) + ) + }, acquireTimeout) + }) + : null, + ].filter((x) => x) + ) + .catch((e: any) => { + if (e && e.isAcquireTimeout) { + throw e + } + + return null + }) + .then(async () => { + // previous operations finished and we didn't get a race on the acquire + // timeout, so the current operation can finally start + return await fn() + }) + + PROCESS_LOCKS[name] = currentOperation + + // finally wait for the current operation to finish successfully, with an + // error or with an acquire timeout error + return await currentOperation +}