@upstash/lock
Version:
A distributed lock implementation using Upstash Redis
184 lines (181 loc) • 6.27 kB
JavaScript
"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