@graphql-mesh/cache-redis
Version:
234 lines (233 loc) • 13.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const ioredis_1 = tslib_1.__importDefault(require("ioredis"));
const ioredis_mock_1 = tslib_1.__importDefault(require("ioredis-mock"));
const cross_helpers_1 = require("@graphql-mesh/cross-helpers");
const string_interpolation_1 = require("@graphql-mesh/string-interpolation");
const types_1 = require("@graphql-mesh/types");
const api_1 = require("@opentelemetry/api");
const disposablestack_1 = require("@whatwg-node/disposablestack");
const iam_js_1 = require("./iam.js");
function interpolateStrWithEnv(str) {
return string_interpolation_1.stringInterpolator.parse(str, { env: cross_helpers_1.process.env });
}
class RedisCache {
constructor(options) {
this.tracer = api_1.trace.getTracer('hive.cache.redis');
this.tracer.startActiveSpan('hive.cache.redis.init', span => {
try {
const lazyConnect = options.lazyConnect !== false;
if ('startupNodes' in options) {
const parsedUsername = interpolateStrWithEnv(options.username?.toString()) || cross_helpers_1.process.env.REDIS_USERNAME;
const parsedPassword = interpolateStrWithEnv(options.password?.toString()) || cross_helpers_1.process.env.REDIS_PASSWORD;
const parsedDb = interpolateStrWithEnv(options.db?.toString()) || cross_helpers_1.process.env.REDIS_DB;
const numDb = parseInt(parsedDb);
const redisOptions = {
username: parsedUsername,
password: parsedPassword,
db: isNaN(numDb) ? undefined : numDb,
enableAutoPipelining: true,
...(lazyConnect ? { lazyConnect: true } : {}),
tls: options.tls ? {} : undefined,
};
this.client = new ioredis_1.default.Cluster(options.startupNodes.map(s => ({
host: s.host && interpolateStrWithEnv(s.host),
port: s.port && parseInt(interpolateStrWithEnv(s.port)),
family: s.family && parseInt(interpolateStrWithEnv(s.family)),
})), {
dnsLookup: options.dnsLookupAsIs
? (address, callback) => callback(null, address)
: undefined,
redisOptions,
enableAutoPipelining: true,
enableOfflineQueue: true,
...(lazyConnect ? { lazyConnect: true } : {}),
});
if (options.iamAuth) {
const clusterRef = this.client;
this.iamInitPromise = (0, iam_js_1.setupIamAuthForCluster)(clusterRef, redisOptions, options.iamAuth, parsedUsername, options.logger)
.then(timer => {
this.iamRefreshTimer = timer;
// connect only after the token is populated so slot discovery authenticates correctly
return clusterRef.connect();
})
.then(() => {
// no need to keep this promise around after init is complete
// it will speed up subsequent get/set calls that would otherwise
// await it unnecessarily
this.iamInitPromise = undefined;
});
}
}
else if ('sentinels' in options) {
this.client = new ioredis_1.default({
name: options.name,
sentinelPassword: options.sentinelPassword && interpolateStrWithEnv(options.sentinelPassword),
sentinels: options.sentinels.map(s => ({
host: s.host && interpolateStrWithEnv(s.host),
port: s.port && parseInt(interpolateStrWithEnv(s.port)),
family: s.family && parseInt(interpolateStrWithEnv(s.family)),
})),
role: options.role,
enableTLSForSentinelMode: options.enableTLSForSentinelMode,
enableAutoPipelining: true,
enableOfflineQueue: true,
lazyConnect,
});
}
else if (options.url) {
const redisUrl = new URL(interpolateStrWithEnv(options.url));
if (!['redis:', 'rediss:'].includes(redisUrl.protocol)) {
throw new Error('Redis URL must use either redis:// or rediss://');
}
if (lazyConnect) {
redisUrl.searchParams.set('lazyConnect', 'true');
}
redisUrl.searchParams.set('enableAutoPipelining', 'true');
redisUrl.searchParams.set('enableOfflineQueue', 'true');
const IPV6_REGEX = /^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$/gm;
if (IPV6_REGEX.test(redisUrl.hostname)) {
redisUrl.searchParams.set('family', '6');
}
const urlStr = redisUrl.toString();
safelyLogURL(options.logger, urlStr);
if (options.iamAuth) {
const redisRef = { current: null };
const connectorUsername = redisUrl.username || undefined;
// username from URL isn't in redisOptions so pass it via buildIamRedisOptions
const iamOpts = (0, iam_js_1.buildIamRedisOptions)({ username: connectorUsername }, options.iamAuth, redisRef);
this.client = new ioredis_1.default(urlStr, iamOpts);
// if you do not set redisRef.current you get an error when connecting
redisRef.current = this.client;
this.iamRefreshTimer = (0, iam_js_1.setupIamAuthRefreshForStandalone)(redisRef, options.iamAuth, connectorUsername, options.logger);
}
else {
this.client = new ioredis_1.default(urlStr);
}
}
else {
const parsedHost = interpolateStrWithEnv(options.host?.toString()) || cross_helpers_1.process.env.REDIS_HOST;
const parsedPort = interpolateStrWithEnv(options.port?.toString()) || cross_helpers_1.process.env.REDIS_PORT;
const parsedUsername = interpolateStrWithEnv(options.username?.toString()) || cross_helpers_1.process.env.REDIS_USERNAME;
const parsedPassword = interpolateStrWithEnv(options.password?.toString()) || cross_helpers_1.process.env.REDIS_PASSWORD;
const parsedDb = interpolateStrWithEnv(options.db?.toString()) || cross_helpers_1.process.env.REDIS_DB;
const parsedFamily = interpolateStrWithEnv(options.family?.toString()) || cross_helpers_1.process.env.REDIS_FAMILY;
const numPort = parseInt(parsedPort);
const numDb = parseInt(parsedDb);
if (parsedHost) {
options.logger.debug(`Connecting to Redis at ${parsedHost}:${parsedPort}`);
const baseOpts = {
host: parsedHost,
port: isNaN(numPort) ? undefined : numPort,
username: parsedUsername,
password: parsedPassword,
db: isNaN(numDb) ? undefined : numDb,
family: parsedFamily === '6' ? 6 : undefined,
...(lazyConnect ? { lazyConnect: true } : {}),
enableAutoPipelining: true,
enableOfflineQueue: true,
};
if (options.iamAuth) {
const redisRef = { current: null };
this.client = new ioredis_1.default((0, iam_js_1.buildIamRedisOptions)(baseOpts, options.iamAuth, redisRef));
redisRef.current = this.client;
this.iamRefreshTimer = (0, iam_js_1.setupIamAuthRefreshForStandalone)(redisRef, options.iamAuth, parsedUsername, options.logger);
}
else {
this.client = new ioredis_1.default(baseOpts);
}
}
else {
options.logger.debug(`Connecting to Redis mock`);
this.client = new ioredis_mock_1.default();
}
}
const pubsub = (0, types_1.toMeshPubSub)(options.pubsub);
// TODO: PubSub.destroy will no longer be needed after v0
const id = pubsub?.subscribe('destroy', () => {
this.client.disconnect(false);
pubsub.unsubscribe(id);
});
}
finally {
span.end();
}
});
}
[disposablestack_1.DisposableSymbols.dispose]() {
if (this.iamRefreshTimer != null) {
clearInterval(this.iamRefreshTimer);
}
this.client.disconnect(false);
}
set(key, value, options) {
return this.tracer.startActiveSpan('hive.cache.set', async (span) => {
try {
if (this.iamInitPromise)
await this.iamInitPromise;
const stringifiedValue = JSON.stringify(value);
if (options?.ttl && options.ttl > 0) {
await this.client.set(key, stringifiedValue, 'PX', options.ttl * 1000);
}
else {
await this.client.set(key, stringifiedValue);
}
}
finally {
span.end();
}
});
}
get(key) {
return this.tracer.startActiveSpan('hive.cache.get', async (span) => {
try {
if (this.iamInitPromise)
await this.iamInitPromise;
const value = await this.client.get(key);
return value != null ? JSON.parse(value) : undefined;
}
finally {
span.end();
}
});
}
getKeysByPrefix(prefix) {
return this.iamInitPromise
? this.iamInitPromise.then(() => scanPatterns(this.client, `${prefix}*`))
: scanPatterns(this.client, `${prefix}*`);
}
delete(key) {
return this.tracer.startActiveSpan('hive.cache.delete', async (span) => {
try {
if (this.iamInitPromise)
await this.iamInitPromise;
const value = await this.client.del(key);
return value > 0;
}
catch {
return false;
}
finally {
span.end();
}
});
}
}
exports.default = RedisCache;
function scanPatterns(redis, pattern, cursor = '0', keys = []) {
return redis.scan(cursor, 'MATCH', pattern, 'COUNT', '10').then(([nextCursor, nextKeys]) => {
keys.push(...nextKeys);
if (nextCursor === '0') {
return keys;
}
return scanPatterns(redis, pattern, nextCursor, keys);
});
}
function safelyLogURL(log, url) {
const logURL = new URL(url);
if (logURL.password) {
logURL.password = '*'.repeat(logURL.password.length);
}
log.debug(`Connecting to Redis at ${logURL}`);
}