UNPKG

@upstash/lock

Version:

A distributed lock implementation using Upstash Redis

184 lines (181 loc) 6.27 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __async = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; // src/index.ts var src_exports = {}; __export(src_exports, { Lock: () => Lock }); module.exports = __toCommonJS(src_exports); // src/lock.ts var Lock = class { constructor(config) { this.DEFAULT_LEASE_MS = 1e4; this.DEFAULT_RETRY_ATTEMPTS = 3; this.DEFAULT_RETRY_DELAY_MS = 100; var _a, _b, _c, _d, _e; this.config = { redis: config.redis, id: config.id, lease: (_a = config.lease) != null ? _a : this.DEFAULT_LEASE_MS, UUID: null, // set when lock is acquired retry: { attempts: (_c = (_b = config.retry) == null ? void 0 : _b.attempts) != null ? _c : this.DEFAULT_RETRY_ATTEMPTS, delay: (_e = (_d = config.retry) == null ? void 0 : _d.delay) != null ? _e : this.DEFAULT_RETRY_DELAY_MS } }; } /** * Tries to acquire a lock with the given configuration. * If initially unsuccessful, the method will retry based on the provided retry configuration. * * @param config - Optional configuration for the lock acquisition to override the constructor config. * @returns {Promise<boolean>} True if the lock was acquired, otherwise false. */ acquire(acquireConfig) { return __async(this, null, function* () { var _a, _b, _c, _d, _e, _f, _g; const lease = (_a = acquireConfig == null ? void 0 : acquireConfig.lease) != null ? _a : this.config.lease; this.config.lease = lease; const retryAttempts = (_d = (_b = acquireConfig == null ? void 0 : acquireConfig.retry) == null ? void 0 : _b.attempts) != null ? _d : (_c = this.config.retry) == null ? void 0 : _c.attempts; const retryDelay = (_g = (_e = acquireConfig == null ? void 0 : acquireConfig.retry) == null ? void 0 : _e.delay) != null ? _g : (_f = this.config.retry) == null ? void 0 : _f.delay; let attempts = 0; let UUID; if (acquireConfig == null ? void 0 : acquireConfig.uuid) { UUID = acquireConfig.uuid; } else { try { UUID = crypto.randomUUID(); } catch (error) { throw new Error("No UUID provided and crypto module is not available in this environment."); } } while (attempts < retryAttempts) { const upstashResult = yield this.config.redis.set(this.config.id, UUID, { nx: true, px: lease }); if (upstashResult === "OK") { this.config.UUID = UUID; return true; } attempts += 1; yield new Promise((resolve) => setTimeout(resolve, retryDelay)); } this.config.UUID = null; return false; }); } /** * Safely releases the lock ensuring the UUID matches. * This operation utilizes a Lua script to interact with Redis and * guarantees atomicity of the unlock operation. * @returns {Promise<boolean>} True if the lock was released, otherwise false. */ release() { return __async(this, null, function* () { const script = ` -- Check if the current UUID still holds the lock if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end `; const numReleased = yield this.config.redis.eval(script, [this.config.id], [this.config.UUID]); return numReleased === 1; }); } /** * Extends the duration for which the lock is held by a given amount of milliseconds. * @param amt - The number of milliseconds by which the lock duration should be extended. * @returns {Promise<boolean>} True if the lock duration was extended, otherwise false. */ extend(amt) { return __async(this, null, function* () { const script = ` -- Check if the current UUID still holds the lock if redis.call("get", KEYS[1]) ~= ARGV[1] then return 0 end -- Get the current TTL and extend it by the specified amount local ttl = redis.call("ttl", KEYS[1]) if ttl > 0 then return redis.call("expire", KEYS[1], ttl + ARGV[2]) else return 0 end `; const extendBy = amt / 1e3; const extended = yield this.config.redis.eval( script, [this.config.id], [this.config.UUID, extendBy] ); if (extended === 1) { this.config.lease += amt; } return extended === 1; }); } get id() { return this.config.id; } /** * Gets the status of the lock, ie: ACQUIRED or FREE. * @returns {Promise<LockStatus>} The status of the lock. */ getStatus() { return __async(this, null, function* () { if (this.config.UUID === null) { return "FREE"; } const UUID = yield this.config.redis.get(this.config.id); if (UUID === this.config.UUID) { return "ACQUIRED"; } return "FREE"; }); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Lock }); //# sourceMappingURL=index.js.map