@backstage/backend-defaults
Version:
Backend defaults used by Backstage backend apps
346 lines (340 loc) • 12.1 kB
JavaScript
'use strict';
var Keyv = require('keyv');
var CacheClient = require('./CacheClient.cjs.js');
var types$1 = require('./types.cjs.js');
var InfinispanOptionsMapper = require('./providers/infinispan/InfinispanOptionsMapper.cjs.js');
var types = require('@backstage/types');
var config = require('@backstage/config');
var InfinispanKeyvStore = require('./providers/infinispan/InfinispanKeyvStore.cjs.js');
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
var Keyv__default = /*#__PURE__*/_interopDefaultCompat(Keyv);
class CacheManager {
/**
* Keys represent supported `backend.cache.store` values, mapped to factories
* that return Keyv instances appropriate to the store.
*/
storeFactories = {
redis: this.createRedisStoreFactory(),
valkey: this.createValkeyStoreFactory(),
memcache: this.createMemcacheStoreFactory(),
memory: this.createMemoryStoreFactory(),
infinispan: this.createInfinispanStoreFactory()
};
logger;
store;
connection;
errorHandler;
defaultTtl;
storeOptions;
/**
* Creates a new {@link CacheManager} instance by reading from the `backend`
* config section, specifically the `.cache` key.
*
* @param config - The loaded application configuration.
* @param options - Optional configuration for the CacheManager.
* @returns A new CacheManager instance.
*/
static fromConfig(config$1, options = {}) {
const store = config$1.getOptionalString("backend.cache.store") || "memory";
const defaultTtlConfig = config$1.getOptional("backend.cache.defaultTtl");
const connectionString = config$1.getOptionalString("backend.cache.connection") || "";
const logger = options.logger?.child({
type: "cacheManager"
});
if (config$1.has("backend.cache.useRedisSets")) {
logger?.warn(
"The 'backend.cache.useRedisSets' configuration key is deprecated and no longer has any effect. The underlying '@keyv/redis' and '@keyv/valkey' libraries no longer support redis sets."
);
}
let defaultTtl;
if (defaultTtlConfig !== void 0) {
if (typeof defaultTtlConfig === "number") {
defaultTtl = defaultTtlConfig;
} else {
defaultTtl = types.durationToMilliseconds(
config.readDurationFromConfig(config$1, { key: "backend.cache.defaultTtl" })
);
}
}
const storeOptions = CacheManager.parseStoreOptions(store, config$1, logger);
return new CacheManager(
store,
connectionString,
options.onError,
logger,
defaultTtl,
storeOptions
);
}
/**
* Parse store-specific options from configuration.
*
* @param store - The cache store type ('redis', 'valkey', 'memcache', 'infinispan', or 'memory')
* @param config - The configuration service
* @param logger - Optional logger for warnings
* @returns The parsed store options
*/
static parseStoreOptions(store, config, logger) {
const storeConfigPath = `backend.cache.${store}`;
if (!config.has(storeConfigPath)) {
logger?.warn(
`No configuration found for cache store '${store}' at '${storeConfigPath}'.`
);
}
if (store === "redis" || store === "valkey") {
return CacheManager.parseRedisOptions(
store,
storeConfigPath,
config,
logger
);
}
if (store === "infinispan") {
return InfinispanOptionsMapper.InfinispanOptionsMapper.parseInfinispanOptions(
storeConfigPath,
config,
logger
);
}
return void 0;
}
/**
* Parse Redis-specific options from configuration.
*/
static parseRedisOptions(store, storeConfigPath, config$1, logger) {
const redisOptions = {
type: store
};
const redisConfig = config$1.getOptionalConfig(storeConfigPath) ?? new config.ConfigReader({});
redisOptions.client = {
namespace: redisConfig.getOptionalString("client.namespace"),
keyPrefixSeparator: redisConfig.getOptionalString("client.keyPrefixSeparator") || ":",
clearBatchSize: redisConfig.getOptionalNumber("client.clearBatchSize"),
useUnlink: redisConfig.getOptionalBoolean("client.useUnlink"),
noNamespaceAffectsAll: redisConfig.getOptionalBoolean(
"client.noNamespaceAffectsAll"
)
};
if (redisConfig.has("cluster")) {
const clusterConfig = redisConfig.getConfig("cluster");
if (!clusterConfig.has("rootNodes")) {
logger?.warn(
`Redis cluster config has no 'rootNodes' key, defaulting to non-clustered mode`
);
return redisOptions;
}
redisOptions.cluster = {
rootNodes: clusterConfig.get("rootNodes"),
defaults: clusterConfig.getOptional("defaults"),
minimizeConnections: clusterConfig.getOptionalBoolean(
"minimizeConnections"
),
useReplicas: clusterConfig.getOptionalBoolean("useReplicas"),
maxCommandRedirections: clusterConfig.getOptionalNumber(
"maxCommandRedirections"
)
};
}
return redisOptions;
}
/** @internal */
constructor(store, connectionString, errorHandler, logger, defaultTtl, storeOptions) {
if (!this.storeFactories.hasOwnProperty(store)) {
throw new Error(`Unknown cache store: ${store}`);
}
this.logger = logger;
this.store = store;
this.connection = connectionString;
this.errorHandler = errorHandler;
this.defaultTtl = defaultTtl;
this.storeOptions = storeOptions;
}
/**
* Generates a PluginCacheManager for consumption by plugins.
*
* @param pluginId - The plugin that the cache manager should be created for.
* Plugin names should be unique.
*/
forPlugin(pluginId) {
const clientFactory = (options) => {
const ttl = options.defaultTtl ?? this.defaultTtl;
return this.getClientWithTtl(
pluginId,
ttl !== void 0 ? types$1.ttlToMilliseconds(ttl) : void 0
);
};
return new CacheClient.DefaultCacheClient(clientFactory({}), clientFactory, {});
}
getClientWithTtl(pluginId, ttl) {
return this.storeFactories[this.store](pluginId, ttl);
}
createRedisStoreFactory() {
const KeyvRedis = require("@keyv/redis").default;
const { createCluster } = require("@keyv/redis");
const stores = {};
return (pluginId, defaultTtl) => {
if (this.storeOptions?.type !== "redis") {
throw new Error(
`Internal error: Wrong config type passed to redis factory: ${this.storeOptions?.type}`
);
}
if (!stores[pluginId]) {
const redisOptions = this.storeOptions?.client || {
keyPrefixSeparator: ":"
};
if (this.storeOptions?.cluster) {
const cluster = createCluster(this.storeOptions?.cluster);
stores[pluginId] = new KeyvRedis(cluster, redisOptions);
} else {
stores[pluginId] = new KeyvRedis(this.connection, redisOptions);
}
stores[pluginId].on("error", (err) => {
this.logger?.error("Failed to create redis cache client", err);
this.errorHandler?.(err);
});
}
return new Keyv__default.default({
namespace: pluginId,
ttl: defaultTtl,
store: stores[pluginId],
emitErrors: false,
useKeyPrefix: false
});
};
}
createValkeyStoreFactory() {
const KeyvValkey = require("@keyv/valkey").default;
const { createCluster } = require("@keyv/valkey");
const stores = {};
return (pluginId, defaultTtl) => {
if (this.storeOptions?.type !== "valkey") {
throw new Error(
`Internal error: Wrong config type passed to valkey factory: ${this.storeOptions?.type}`
);
}
if (!stores[pluginId]) {
const valkeyOptions = this.storeOptions?.client || {
keyPrefixSeparator: ":"
};
if (this.storeOptions?.cluster) {
const cluster = createCluster(this.storeOptions?.cluster);
stores[pluginId] = new KeyvValkey(cluster, valkeyOptions);
} else {
stores[pluginId] = new KeyvValkey(this.connection, valkeyOptions);
}
stores[pluginId].on("error", (err) => {
this.logger?.error("Failed to create valkey cache client", err);
this.errorHandler?.(err);
});
}
return new Keyv__default.default({
namespace: pluginId,
ttl: defaultTtl,
store: stores[pluginId],
emitErrors: false,
useKeyPrefix: false
});
};
}
createMemcacheStoreFactory() {
const KeyvMemcache = require("@keyv/memcache").default;
const stores = {};
return (pluginId, defaultTtl) => {
if (!stores[pluginId]) {
stores[pluginId] = new KeyvMemcache(this.connection);
stores[pluginId].on("error", (err) => {
this.logger?.error("Failed to create memcache cache client", err);
this.errorHandler?.(err);
});
}
return new Keyv__default.default({
namespace: pluginId,
ttl: defaultTtl,
emitErrors: false,
store: stores[pluginId]
});
};
}
createMemoryStoreFactory() {
const store = /* @__PURE__ */ new Map();
return (pluginId, defaultTtl) => new Keyv__default.default({
namespace: pluginId,
ttl: defaultTtl,
emitErrors: false,
store
});
}
createInfinispanStoreFactory() {
const stores = {};
return (pluginId, defaultTtl) => {
if (this.storeOptions?.type !== "infinispan") {
throw new Error(
`Internal error: Wrong config type passed to infinispan factory: ${this.storeOptions?.type}`
);
}
if (!stores[pluginId]) {
const isTest = process.env.NODE_ENV === "test" || typeof jest !== "undefined";
const clientPromise = isTest ? this.createInfinispanClientSync() : this.createInfinispanClientAsync();
this.logger?.info(
`Creating Infinispan cache client for plugin ${pluginId} isTest = ${isTest}`
);
const storeInstance = new InfinispanKeyvStore.InfinispanKeyvStore({
clientPromise,
logger: this.logger
});
stores[pluginId] = storeInstance;
storeInstance.on("error", (err) => {
this.logger?.error("Failed to create infinispan cache client", err);
this.errorHandler?.(err);
});
}
return new Keyv__default.default({
namespace: pluginId,
ttl: defaultTtl,
store: stores[pluginId],
emitErrors: false
});
};
}
/**
* Creates an Infinispan client using dynamic import (production use).
* @returns Promise that resolves to an Infinispan client
*/
async createInfinispanClientAsync() {
return this.createInfinispanClient(false);
}
/**
* Creates an Infinispan client using synchronous import (testing purposes).
* @returns Promise that resolves to an Infinispan client
*/
createInfinispanClientSync() {
return this.createInfinispanClient(true);
}
/**
* Creates an Infinispan client based on the provided configuration.
* @param useSync - Whether to use synchronous import (for testing) or dynamic import
* @returns Promise that resolves to an Infinispan client
*/
async createInfinispanClient(useSync = false) {
try {
this.logger?.info("Creating Infinispan client");
if (this.storeOptions?.type === "infinispan") {
const infinispan = useSync ? require("infinispan") : await import('infinispan');
const client = await infinispan.client(
this.storeOptions.servers,
this.storeOptions.options
);
this.logger?.info("Infinispan client created successfully");
return client;
}
throw new Error("Infinispan store options are not defined");
} catch (error) {
this.logger?.error("Failed to create Infinispan client", {
error: error.message
});
throw error;
}
}
}
exports.CacheManager = CacheManager;
//# sourceMappingURL=CacheManager.cjs.js.map