@upstash/lock
Version:
A distributed lock implementation using Upstash Redis
158 lines (156 loc) • 5.26 kB
JavaScript
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/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";
});
}
};
export {
Lock
};
//# sourceMappingURL=index.mjs.map