@jbagatta/johnny-cache
Version:
A robust distributed dictionary for coordinating and caching expensive operations in a distributed environment
104 lines (103 loc) • 3.83 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createRedisDataStore = exports.RedisConnectionOptions = exports.tryUpdateReservationLuaScript = exports.tryReserveAndReturnExistingBuildLuaScript = exports.RedisDataStore = void 0;
const ioredis_1 = require("ioredis");
const uuid_1 = require("uuid");
const buildIdField = 'buildId';
const buildResultField = 'buildResult';
class RedisDataStore {
constructor(client) {
this.client = client;
}
async tryReserve(key, reservationExpiryMs) {
const buildId = (0, uuid_1.v4)();
const result = await this.client.eval(exports.tryReserveAndReturnExistingBuildLuaScript, 1, key, buildId, reservationExpiryMs);
return {
isNew: result[0] === buildId,
buildId: result[0],
completedBuild: result[1] ? JSON.parse(result[1]) : null
};
}
async has(key) {
return await this.client.exists(key) === 1;
}
async get(key) {
const result = await this.client.hgetall(key);
if (Object.keys(result).length === 0) {
return null;
}
return {
isNew: false,
buildId: result[buildIdField],
completedBuild: buildResultField in result ? JSON.parse(result[buildResultField]) : null
};
}
async tryUpdateReservation(key, buildId, result, expiry) {
return await this.client.eval(exports.tryUpdateReservationLuaScript, 1, key, buildId, JSON.stringify(result), expiry ?? -1);
}
async delete(key) {
await this.client.del(key);
}
async updateExpiry(key, expiry) {
if (expiry) {
await this.client.pexpire(key, expiry);
}
}
close() {
this.client.disconnect();
}
}
exports.RedisDataStore = RedisDataStore;
exports.tryReserveAndReturnExistingBuildLuaScript = ` \
local isNew = redis.call('HSETNX', KEYS[1], '${buildIdField}', ARGV[1]) \
if (isNew == 1) then \
redis.call('PEXPIRE', KEYS[1], ARGV[2]) \
return {ARGV[1], nil} \
else \
local buildKey = redis.call('HGET', KEYS[1], '${buildIdField}') \
local buildResult = redis.call('HGET', KEYS[1], '${buildResultField}') \
return {buildKey, buildResult} \
end \
`;
exports.tryUpdateReservationLuaScript = ` \
local exists = redis.call('EXISTS', KEYS[1]) \
if (exists == 0) then \
redis.call('HSET', KEYS[1], '${buildIdField}', ARGV[1]) \
end \
if (redis.call('HGET', KEYS[1], '${buildIdField}') == ARGV[1]) then \
redis.call('HSET', KEYS[1], '${buildResultField}', ARGV[2]) \
if (ARGV[3] == -1) then \
redis.call('PERSIST', KEYS[1]) \
else \
redis.call('PEXPIRE', KEYS[1], ARGV[3]) \
end \
return true \
else \
return false \
end \
`;
class RedisConnectionOptions {
}
exports.RedisConnectionOptions = RedisConnectionOptions;
function createRedisDataStore(redisConnectionOptions) {
if (!redisConnectionOptions.sentinel && !redisConnectionOptions.url) {
throw new Error("Missing Redis connection configration (sentinel or url)");
}
let redisClient;
if (redisConnectionOptions.sentinel) {
redisClient = new ioredis_1.Redis({
sentinels: [{ host: redisConnectionOptions.sentinel.host, port: redisConnectionOptions.sentinel.port }],
name: redisConnectionOptions.sentinel.primaryName,
password: redisConnectionOptions.password,
sentinelPassword: redisConnectionOptions.password
});
}
else {
redisClient = new ioredis_1.Redis({
path: redisConnectionOptions.url,
password: redisConnectionOptions.password
});
}
return new RedisDataStore(redisClient);
}
exports.createRedisDataStore = createRedisDataStore;