feat: new cache manager infinispan
Signed-off-by: John Redwood <john.r.k.redwood@gmail.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/backend-test-utils': minor
|
||||
'@backstage/backend-defaults': minor
|
||||
---
|
||||
|
||||
feat: new cache manager `Infinispan Data Grid`
|
||||
@@ -199,6 +199,7 @@ impactful
|
||||
incentivised
|
||||
Indal
|
||||
indexable
|
||||
infinispan
|
||||
ingestors
|
||||
inlined
|
||||
inlinehilite
|
||||
|
||||
@@ -284,8 +284,8 @@ stores as a means of improving performance or reliability. Similar to how
|
||||
databases are supported, plugins receive logically separated cache connections,
|
||||
which are powered by [Keyv](https://github.com/lukechilds/keyv) under the hood.
|
||||
|
||||
At this time of writing, Backstage can be configured to use one of three cache
|
||||
stores: memory, which is mainly used for local testing, memcache or Redis,
|
||||
At this time of writing, Backstage can be configured to use one of five cache
|
||||
stores: memory, which is mainly used for local testing, memcache, redis, valkey or infinispan,
|
||||
which are cache stores better suited for production deployment. The right cache
|
||||
store for your Backstage instance will depend on your own run-time constraints
|
||||
and those required of the plugins you're running.
|
||||
@@ -316,6 +316,40 @@ backend:
|
||||
connection: redis://user:pass@cache.example.com:6379
|
||||
```
|
||||
|
||||
### Use Infinispan for cache
|
||||
|
||||
#### Minimal configuration
|
||||
|
||||
- defaults, no `authentication`, expects cache named `cache` and host `127.0.0.1:11222`)
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
cache:
|
||||
store: infinispan
|
||||
```
|
||||
|
||||
#### Extended configuration
|
||||
|
||||
- Unlike Redis, Infinispan will **not** create the cache for you. It's expected you've configured the cache in your infinispan server prior to configuration here.
|
||||
- A full list of configuration items are available: https://docs.jboss.org/infinispan/hotrod-clients/javascript/1.0/apidocs/module-infinispan.html including support for backup clusters.
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
cache:
|
||||
store: infinispan
|
||||
infinispan:
|
||||
servers:
|
||||
- host: 127.0.0.1
|
||||
port: 11222
|
||||
cacheName: backstage-cache
|
||||
mediaType: application/json
|
||||
authentication:
|
||||
enabled: true
|
||||
userName: yourusername
|
||||
password: yourpassword
|
||||
saslMechanism: PLAIN
|
||||
```
|
||||
|
||||
Contributions supporting other cache stores are welcome!
|
||||
|
||||
## Containerization
|
||||
|
||||
+179
@@ -750,6 +750,185 @@ export interface Config {
|
||||
connection: string;
|
||||
/** An optional default TTL (in milliseconds). */
|
||||
defaultTtl?: number | HumanDuration | string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Infinispan cache store configuration.
|
||||
* @see https://docs.jboss.org/infinispan/hotrod-clients/javascript/1.0/apidocs/module-infinispan.html
|
||||
*/
|
||||
store: 'infinispan';
|
||||
|
||||
/**
|
||||
* An optional default TTL (in milliseconds).
|
||||
*/
|
||||
defaultTtl?: number | HumanDuration | string;
|
||||
|
||||
/**
|
||||
* Configuration for the Infinispan cache store.
|
||||
*/
|
||||
infinispan?: {
|
||||
/**
|
||||
* Version of client/server protocol.
|
||||
* @default '2.9' is the latest version.
|
||||
*/
|
||||
version?: '2.9' | '2.5' | '2.2';
|
||||
|
||||
/**
|
||||
* Infinispan Cache Name if not provided default is `cache` recommended to set this.
|
||||
*/
|
||||
cacheName?: string;
|
||||
|
||||
/**
|
||||
* Optional number of retries for operation.
|
||||
* Defaults to 3.
|
||||
*/
|
||||
maxRetries?: number;
|
||||
|
||||
/**
|
||||
* Optional flag to controls whether the client deals with topology updates or not.
|
||||
* @default true
|
||||
*/
|
||||
topologyUpdates?: boolean;
|
||||
|
||||
/**
|
||||
* Media type of the cache contents.
|
||||
* @default 'text/plain'
|
||||
*/
|
||||
mediaType?: 'text/plain' | 'application/json';
|
||||
|
||||
/**
|
||||
* Infinispan server host and port configuration.
|
||||
* If this is an array, the client will connect to all servers in the list based on TOPOLOGY_AWARE routing.
|
||||
* If this is a single object, it will be used as the default server.
|
||||
*/
|
||||
servers?:
|
||||
| Array<{
|
||||
/**
|
||||
* Infinispan server host.
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* Infinispan server port (Hot Rod protocol). Defaults to `11222`.
|
||||
*/
|
||||
port?: number;
|
||||
}>
|
||||
| {
|
||||
/**
|
||||
* Infinispan server host. Defaults to `127.0.0.1`.
|
||||
*/
|
||||
host?: string;
|
||||
/**
|
||||
* Infinispan server port (Hot Rod protocol). Defaults to `11222`.
|
||||
*/
|
||||
port?: number;
|
||||
};
|
||||
authentication?: {
|
||||
/**
|
||||
* Enable authentication. Defaults to `false`.
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Select the SASL mechanism to use. Can be one of PLAIN, DIGEST-MD5, SCRAM-SHA-1, SCRAM-SHA-256, SCRAM-SHA-384, SCRAM-SHA-512, EXTERNAL, OAUTHBEARER
|
||||
*/
|
||||
saslMechanism?: string;
|
||||
/**
|
||||
* userName for authentication.
|
||||
*/
|
||||
userName?: string;
|
||||
/**
|
||||
* Password for authentication.
|
||||
* @visibility secret
|
||||
*/
|
||||
password?: string;
|
||||
/**
|
||||
* The OAuth token. Required by the OAUTHBEARER mechanism.
|
||||
* @visibility secret
|
||||
*/
|
||||
token?: string;
|
||||
/**
|
||||
* The SASL authorization ID.
|
||||
*/
|
||||
authzid?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* TLS/SSL configuration.
|
||||
*/
|
||||
ssl?: {
|
||||
/**
|
||||
* Enable ssl connection. Defaults to `false`.
|
||||
* @default false
|
||||
*/
|
||||
enabled?: boolean;
|
||||
|
||||
/**
|
||||
* Optional field with secure protocol in use.
|
||||
* @default TLSv1_2_method
|
||||
*/
|
||||
secureProtocol?: string;
|
||||
|
||||
/**
|
||||
* Optional paths of trusted SSL certificates.
|
||||
*/
|
||||
trustCerts?: Array<string>;
|
||||
|
||||
clientAuth?: {
|
||||
/**
|
||||
* Optional path to client authentication key
|
||||
*/
|
||||
key?: string;
|
||||
/**
|
||||
* Optional password for client key
|
||||
*/
|
||||
passphrase?: string;
|
||||
/**
|
||||
* Optional client certificate
|
||||
*/
|
||||
cert?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Optional SNI host name.
|
||||
*/
|
||||
sniHostName?: string;
|
||||
|
||||
/**
|
||||
* Optional crypto store configuration.
|
||||
*/
|
||||
cryptoStore?: {
|
||||
/** Optional crypto store path. */
|
||||
path?: string;
|
||||
/** Optional password for crypto store. */
|
||||
passphrase?: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Optional additional clusters for cross-site failovers.
|
||||
* Array.<Cluster>
|
||||
*/
|
||||
clusters?: Array<{
|
||||
/**
|
||||
* Optional Cluster name
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Cluster servers details.
|
||||
* Array.<ServerAddress>
|
||||
*/
|
||||
servers: Array<{
|
||||
/**
|
||||
* Infinispan cluster server host.
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* Infinispan server port (Hot Rod protocol). Defaults to `11222`.
|
||||
*/
|
||||
port?: number;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
cors?: {
|
||||
|
||||
@@ -168,6 +168,7 @@
|
||||
"fs-extra": "^11.2.0",
|
||||
"git-url-parse": "^15.0.0",
|
||||
"helmet": "^6.0.0",
|
||||
"infinispan": "^0.12.0",
|
||||
"is-glob": "^4.0.3",
|
||||
"jose": "^5.0.0",
|
||||
"keyv": "^5.2.1",
|
||||
|
||||
+168
-41
@@ -27,9 +27,16 @@ import {
|
||||
ttlToMilliseconds,
|
||||
CacheStoreOptions,
|
||||
RedisCacheStoreOptions,
|
||||
InfinispanClientBehaviorOptions,
|
||||
InfinispanServerConfig,
|
||||
} from './types';
|
||||
import { InfinispanOptionsMapper } from './providers/infinispan/InfinispanOptionsMapper';
|
||||
import { durationToMilliseconds } from '@backstage/types';
|
||||
import { readDurationFromConfig } from '@backstage/config';
|
||||
import { ConfigReader, readDurationFromConfig } from '@backstage/config';
|
||||
import {
|
||||
InfinispanClientCacheInterface,
|
||||
InfinispanKeyvStore,
|
||||
} from './providers/infinispan/InfinispanKeyvStore';
|
||||
|
||||
type StoreFactory = (pluginId: string, defaultTtl: number | undefined) => Keyv;
|
||||
|
||||
@@ -50,6 +57,7 @@ export class CacheManager {
|
||||
valkey: this.createValkeyStoreFactory(),
|
||||
memcache: this.createMemcacheStoreFactory(),
|
||||
memory: this.createMemoryStoreFactory(),
|
||||
infinispan: this.createInfinispanStoreFactory(),
|
||||
};
|
||||
|
||||
private readonly logger?: LoggerService;
|
||||
@@ -64,6 +72,8 @@ export class CacheManager {
|
||||
* 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: RootConfigService,
|
||||
@@ -112,7 +122,7 @@ export class CacheManager {
|
||||
/**
|
||||
* Parse store-specific options from configuration.
|
||||
*
|
||||
* @param store - The cache store type ('redis', 'valkey', 'memcache', or 'memory')
|
||||
* @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
|
||||
@@ -124,11 +134,27 @@ export class CacheManager {
|
||||
): CacheStoreOptions | undefined {
|
||||
const storeConfigPath = `backend.cache.${store}`;
|
||||
|
||||
if (
|
||||
(store === 'redis' || store === 'valkey') &&
|
||||
config.has(storeConfigPath)
|
||||
) {
|
||||
return CacheManager.parseRedisOptions(storeConfigPath, config, logger);
|
||||
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.parseInfinispanOptions(
|
||||
storeConfigPath,
|
||||
config,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -138,12 +164,17 @@ export class CacheManager {
|
||||
* Parse Redis-specific options from configuration.
|
||||
*/
|
||||
private static parseRedisOptions(
|
||||
store: string,
|
||||
storeConfigPath: string,
|
||||
config: RootConfigService,
|
||||
logger?: LoggerService,
|
||||
): RedisCacheStoreOptions {
|
||||
const redisOptions: RedisCacheStoreOptions = {};
|
||||
const redisConfig = config.getConfig(storeConfigPath);
|
||||
const redisOptions: RedisCacheStoreOptions = {
|
||||
type: store as 'redis' | 'valkey',
|
||||
};
|
||||
|
||||
const redisConfig =
|
||||
config.getOptionalConfig(storeConfigPath) ?? new ConfigReader({});
|
||||
|
||||
redisOptions.client = {
|
||||
namespace: redisConfig.getOptionalString('client.namespace'),
|
||||
@@ -231,23 +262,25 @@ export class CacheManager {
|
||||
|
||||
return (pluginId, defaultTtl) => {
|
||||
if (!stores[pluginId]) {
|
||||
const redisOptions = this.storeOptions?.client || {
|
||||
keyPrefixSeparator: ':',
|
||||
};
|
||||
if (this.storeOptions?.cluster) {
|
||||
// Create a Redis cluster
|
||||
const cluster = createCluster(this.storeOptions?.cluster);
|
||||
stores[pluginId] = new KeyvRedis(cluster, redisOptions);
|
||||
} else {
|
||||
// Create a regular Redis connection
|
||||
stores[pluginId] = new KeyvRedis(this.connection, redisOptions);
|
||||
}
|
||||
if (this.storeOptions?.type === 'redis') {
|
||||
const redisOptions = this.storeOptions?.client || {
|
||||
keyPrefixSeparator: ':',
|
||||
};
|
||||
if (this.storeOptions?.cluster) {
|
||||
// Create a Redis cluster
|
||||
const cluster = createCluster(this.storeOptions?.cluster);
|
||||
stores[pluginId] = new KeyvRedis(cluster, redisOptions);
|
||||
} else {
|
||||
// Create a regular Redis connection
|
||||
stores[pluginId] = new KeyvRedis(this.connection, redisOptions);
|
||||
}
|
||||
|
||||
// Always provide an error handler to avoid stopping the process
|
||||
stores[pluginId].on('error', (err: Error) => {
|
||||
this.logger?.error('Failed to create redis cache client', err);
|
||||
this.errorHandler?.(err);
|
||||
});
|
||||
// Always provide an error handler to avoid stopping the process
|
||||
stores[pluginId].on('error', (err: Error) => {
|
||||
this.logger?.error('Failed to create redis cache client', err);
|
||||
this.errorHandler?.(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
return new Keyv({
|
||||
namespace: pluginId,
|
||||
@@ -266,23 +299,25 @@ export class CacheManager {
|
||||
|
||||
return (pluginId, defaultTtl) => {
|
||||
if (!stores[pluginId]) {
|
||||
const valkeyOptions = this.storeOptions?.client || {
|
||||
keyPrefixSeparator: ':',
|
||||
};
|
||||
if (this.storeOptions?.cluster) {
|
||||
// Create a Valkey cluster (Redis cluster under the hood)
|
||||
const cluster = createCluster(this.storeOptions?.cluster);
|
||||
stores[pluginId] = new KeyvValkey(cluster, valkeyOptions);
|
||||
} else {
|
||||
// Create a regular Valkey connection
|
||||
stores[pluginId] = new KeyvValkey(this.connection, valkeyOptions);
|
||||
}
|
||||
if (this.storeOptions?.type === 'valkey') {
|
||||
const valkeyOptions = this.storeOptions?.client || {
|
||||
keyPrefixSeparator: ':',
|
||||
};
|
||||
if (this.storeOptions?.cluster) {
|
||||
// Create a Valkey cluster (Redis cluster under the hood)
|
||||
const cluster = createCluster(this.storeOptions?.cluster);
|
||||
stores[pluginId] = new KeyvValkey(cluster, valkeyOptions);
|
||||
} else {
|
||||
// Create a regular Valkey connection
|
||||
stores[pluginId] = new KeyvValkey(this.connection, valkeyOptions);
|
||||
}
|
||||
|
||||
// Always provide an error handler to avoid stopping the process
|
||||
stores[pluginId].on('error', (err: Error) => {
|
||||
this.logger?.error('Failed to create valkey cache client', err);
|
||||
this.errorHandler?.(err);
|
||||
});
|
||||
// Always provide an error handler to avoid stopping the process
|
||||
stores[pluginId].on('error', (err: Error) => {
|
||||
this.logger?.error('Failed to create valkey cache client', err);
|
||||
this.errorHandler?.(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
return new Keyv({
|
||||
namespace: pluginId,
|
||||
@@ -326,4 +361,96 @@ export class CacheManager {
|
||||
store,
|
||||
});
|
||||
}
|
||||
|
||||
private createInfinispanStoreFactory(): StoreFactory {
|
||||
const stores: Record<string, InfinispanKeyvStore> = {};
|
||||
|
||||
return (pluginId, defaultTtl) => {
|
||||
if (!stores[pluginId]) {
|
||||
if (this.storeOptions?.type === 'infinispan') {
|
||||
// Use sync version for testing environments
|
||||
const isTest =
|
||||
process.env.NODE_ENV === 'test' || typeof jest !== 'undefined';
|
||||
|
||||
// Create the client promise ONCE and reuse it
|
||||
const clientPromise: Promise<InfinispanClientCacheInterface> = isTest
|
||||
? this.createInfinispanClientSync()
|
||||
: this.createInfinispanClientAsync();
|
||||
|
||||
this.logger?.info(
|
||||
`Creating Infinispan cache client for plugin ${pluginId} isTest = ${isTest}`,
|
||||
);
|
||||
const storeInstance = new InfinispanKeyvStore({
|
||||
clientPromise,
|
||||
logger: this.logger!,
|
||||
});
|
||||
|
||||
stores[pluginId] = storeInstance;
|
||||
|
||||
// Always provide an error handler to avoid stopping the process
|
||||
storeInstance.on('error', (err: Error) => {
|
||||
this.logger?.error('Failed to create infinispan cache client', err);
|
||||
this.errorHandler?.(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new Keyv({
|
||||
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
|
||||
*/
|
||||
private async createInfinispanClientAsync(): Promise<InfinispanClientCacheInterface> {
|
||||
return this.createInfinispanClient(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Infinispan client using synchronous import (testing purposes).
|
||||
* @returns Promise that resolves to an Infinispan client
|
||||
*/
|
||||
private createInfinispanClientSync(): Promise<InfinispanClientCacheInterface> {
|
||||
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
|
||||
*/
|
||||
private async createInfinispanClient(
|
||||
useSync: boolean = false,
|
||||
): Promise<InfinispanClientCacheInterface> {
|
||||
try {
|
||||
this.logger?.info('Creating Infinispan client');
|
||||
|
||||
if (this.storeOptions?.type === 'infinispan') {
|
||||
// Import infinispan based on the useSync parameter
|
||||
const infinispan = useSync
|
||||
? require('infinispan')
|
||||
: await import('infinispan');
|
||||
|
||||
const client = await infinispan.client(
|
||||
this.storeOptions.servers as InfinispanServerConfig[],
|
||||
this.storeOptions.options as InfinispanClientBehaviorOptions,
|
||||
);
|
||||
|
||||
this.logger?.info('Infinispan client created successfully');
|
||||
return client;
|
||||
}
|
||||
throw new Error('Infinispan store options are not defined');
|
||||
} catch (error: any) {
|
||||
this.logger?.error('Failed to create Infinispan client', {
|
||||
error: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+177
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InfinispanKeyvStore } from './InfinispanKeyvStore';
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('InfinispanKeyvStore', () => {
|
||||
const logger = mockServices.logger.mock();
|
||||
let store: InfinispanKeyvStore;
|
||||
let mockClient: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = {
|
||||
get: jest.fn(),
|
||||
put: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
on: jest.fn(),
|
||||
};
|
||||
|
||||
store = new InfinispanKeyvStore({
|
||||
clientPromise: Promise.resolve(mockClient),
|
||||
logger,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('returns undefined when key is not found', async () => {
|
||||
mockClient.get.mockResolvedValueOnce(null);
|
||||
const result = await store.get('test-key');
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockClient.get).toHaveBeenCalledWith('test-key');
|
||||
});
|
||||
|
||||
it('returns value when key is found', async () => {
|
||||
const value = 'test-value';
|
||||
mockClient.get.mockResolvedValueOnce(value);
|
||||
const result = await store.get('test-key');
|
||||
expect(result).toBe(value);
|
||||
expect(mockClient.get).toHaveBeenCalledWith('test-key');
|
||||
});
|
||||
|
||||
it('handles client errors', async () => {
|
||||
const error = new Error('Connection error');
|
||||
mockClient.get.mockRejectedValueOnce(error);
|
||||
await expect(store.get('test-key')).rejects.toThrow('Connection error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('set', () => {
|
||||
it('sets value with default options', async () => {
|
||||
await store.set('test-key', 'test-value');
|
||||
expect(mockClient.put).toHaveBeenCalledWith('test-key', 'test-value', {});
|
||||
});
|
||||
|
||||
it('sets value with TTL', async () => {
|
||||
await store.set('test-key', 'test-value', 1000);
|
||||
expect(mockClient.put).toHaveBeenCalledWith('test-key', 'test-value', {
|
||||
lifespan: '1000ms',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles client errors', async () => {
|
||||
const error = new Error('Connection error');
|
||||
mockClient.put.mockRejectedValueOnce(error);
|
||||
await expect(store.set('test-key', 'test-value')).rejects.toThrow(
|
||||
'Connection error',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('deletes key', async () => {
|
||||
mockClient.remove.mockResolvedValueOnce(true);
|
||||
const result = await store.delete('test-key');
|
||||
expect(result).toBe(true);
|
||||
expect(mockClient.remove).toHaveBeenCalledWith('test-key');
|
||||
});
|
||||
|
||||
it('handles client errors', async () => {
|
||||
const error = new Error('Connection error');
|
||||
mockClient.remove.mockRejectedValueOnce(error);
|
||||
await expect(store.delete('test-key')).rejects.toThrow(
|
||||
'Connection error',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('clears all entries', async () => {
|
||||
await store.clear();
|
||||
expect(mockClient.clear).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles client errors', async () => {
|
||||
const error = new Error('Connection error');
|
||||
mockClient.clear.mockRejectedValueOnce(error);
|
||||
await expect(store.clear()).rejects.toThrow('Connection error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('disconnect', () => {
|
||||
it('is a no-op as client is managed by CacheManager', async () => {
|
||||
const disconnectMockClient = {
|
||||
disconnect: jest.fn().mockResolvedValue(undefined),
|
||||
get: jest.fn().mockResolvedValue(null),
|
||||
put: jest.fn().mockResolvedValue(undefined),
|
||||
remove: jest.fn().mockResolvedValue(undefined),
|
||||
clear: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const disconnectStore = new InfinispanKeyvStore({
|
||||
clientPromise: Promise.resolve(disconnectMockClient),
|
||||
logger: mockServices.logger.mock(),
|
||||
});
|
||||
await disconnectStore.disconnect();
|
||||
expect(disconnectMockClient.disconnect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles client errors', async () => {
|
||||
const error = new Error('Connection error');
|
||||
const errorMockClient = {
|
||||
disconnect: jest.fn().mockRejectedValue(error),
|
||||
get: jest.fn().mockResolvedValue(null),
|
||||
put: jest.fn().mockResolvedValue(undefined),
|
||||
remove: jest.fn().mockResolvedValue(undefined),
|
||||
clear: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const errorStore = new InfinispanKeyvStore({
|
||||
clientPromise: Promise.resolve(errorMockClient),
|
||||
logger: mockServices.logger.mock(),
|
||||
});
|
||||
await expect(errorStore.disconnect()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('emits error events from client', async () => {
|
||||
const error = new Error('Client error');
|
||||
const errorHandler = jest.fn();
|
||||
store.on('error', errorHandler);
|
||||
|
||||
// Simulate client error
|
||||
const clientPromise = Promise.resolve(mockClient);
|
||||
store = new InfinispanKeyvStore({
|
||||
clientPromise,
|
||||
logger,
|
||||
});
|
||||
|
||||
// Wait for client to be resolved
|
||||
await clientPromise;
|
||||
|
||||
// Trigger error event
|
||||
const errorCallback = mockClient.on.mock.calls[0][1];
|
||||
errorCallback(error);
|
||||
|
||||
expect(errorHandler).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
Vendored
+206
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { InfinispanPutOptions } from '../../types';
|
||||
|
||||
/**
|
||||
* Interface defining the required methods for an Infinispan client.
|
||||
* @public
|
||||
*/
|
||||
export interface InfinispanClientCacheInterface {
|
||||
get(key: string): Promise<string | null | undefined>;
|
||||
put(key: string, value: string, options?: InfinispanPutOptions): Promise<any>;
|
||||
remove(key: string): Promise<boolean>;
|
||||
clear(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
on?(event: 'error' | string, listener: (...args: any[]) => void): this;
|
||||
connect?(): Promise<any>;
|
||||
query?(query: string): Promise<any[] | null>;
|
||||
containsKey?(key: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating an InfinispanKeyvStore instance.
|
||||
*/
|
||||
export interface InfinispanKeyvStoreOptions {
|
||||
clientPromise: Promise<InfinispanClientCacheInterface>;
|
||||
logger: LoggerService;
|
||||
defaultTtl?: number; // TTL in milliseconds
|
||||
}
|
||||
|
||||
/**
|
||||
* A Keyv store implementation that uses Infinispan as the backend.
|
||||
* This store implements the Keyv store interface and provides caching functionality
|
||||
* using Infinispan's distributed cache capabilities.
|
||||
*/
|
||||
export class InfinispanKeyvStore extends EventEmitter {
|
||||
private readonly clientPromise: Promise<InfinispanClientCacheInterface>;
|
||||
private readonly logger: LoggerService;
|
||||
private readonly defaultTtl?: number;
|
||||
private resolvedClient: InfinispanClientCacheInterface | null = null;
|
||||
|
||||
public readonly namespace?: string; // Keyv expects this
|
||||
|
||||
constructor(options: InfinispanKeyvStoreOptions) {
|
||||
super();
|
||||
this.clientPromise = options.clientPromise;
|
||||
this.logger = options.logger.child({ class: InfinispanKeyvStore.name });
|
||||
this.defaultTtl = options.defaultTtl;
|
||||
|
||||
// Eagerly try to resolve the client to attach error listeners early
|
||||
// and to have it ready for disconnect if resolved.
|
||||
this.clientPromise
|
||||
.then(client => {
|
||||
this.resolvedClient = client;
|
||||
if (typeof client.on === 'function') {
|
||||
client.on('error', (error: Error) => {
|
||||
this.logger.error('Native Infinispan client reported an error.', {
|
||||
error: error.message,
|
||||
});
|
||||
this.emit('error', error);
|
||||
});
|
||||
} else {
|
||||
this.logger.warn(
|
||||
'Native Infinispan client does not appear to support .on("error") event listening.',
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.logger.error(
|
||||
'Failed to resolve Infinispan client promise in constructor.',
|
||||
{ error: err.message },
|
||||
);
|
||||
// Errors from operations will also be emitted when clientPromise is awaited and rejects.
|
||||
this.emit('error', err);
|
||||
});
|
||||
}
|
||||
|
||||
private async getClient(): Promise<InfinispanClientCacheInterface> {
|
||||
if (this.resolvedClient) {
|
||||
return this.resolvedClient;
|
||||
}
|
||||
// If not yet resolved (e.g. called very quickly or promise rejected and retrying implicitly)
|
||||
// Await the promise. This will throw if the promise is rejected.
|
||||
this.resolvedClient = await this.clientPromise;
|
||||
return this.resolvedClient;
|
||||
}
|
||||
|
||||
async get(key: string): Promise<string | undefined> {
|
||||
this.logger.debug(`Getting key: ${key}`);
|
||||
try {
|
||||
const client = await this.getClient();
|
||||
const value = await client.get(key);
|
||||
if (value === null || value === undefined) {
|
||||
this.logger.debug(`Key not found or value is null/undefined: ${key}`);
|
||||
return undefined;
|
||||
}
|
||||
this.logger.debug(`Successfully retrieved key: ${key}`);
|
||||
return value;
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error getting key '${key}' from Infinispan.`, {
|
||||
error: error.message,
|
||||
});
|
||||
this.emit(
|
||||
'error',
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async set(key: string, value: string, ttl?: number): Promise<void> {
|
||||
this.logger.debug(`Setting key: ${key}`, { ttl });
|
||||
this.logger.debug(`Setting key: ${key}`, { ttlInput: ttl });
|
||||
const currentTtl = ttl ?? this.defaultTtl;
|
||||
this.logger.debug(`Calculated currentTtl for key ${key}: ${currentTtl}ms`); // Log do TTL calculado
|
||||
const storeOptions: InfinispanPutOptions = {};
|
||||
|
||||
if (typeof currentTtl === 'number' && currentTtl > 0) {
|
||||
storeOptions.lifespan = `${currentTtl}ms`; // Ensure time unit is passed as string
|
||||
// Ensure this matches client expectations. If client expects string '10s', convert here.
|
||||
// For now, assuming number in ms is fine or string like '10000ms'.
|
||||
// The PutOptions defines lifespan as string | number | null.
|
||||
} else if (typeof currentTtl === 'string') {
|
||||
storeOptions.lifespan = currentTtl;
|
||||
}
|
||||
|
||||
try {
|
||||
const client = await this.getClient();
|
||||
await client.put(key, value, storeOptions);
|
||||
this.logger.debug(`Successfully set key: ${key}`);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error setting key '${key}' in Infinispan.`, {
|
||||
error: error.message,
|
||||
});
|
||||
this.emit(
|
||||
'error',
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<boolean> {
|
||||
this.logger.debug(`Deleting key: ${key}`);
|
||||
try {
|
||||
const client = await this.getClient();
|
||||
const deleted = await client.remove(key);
|
||||
this.logger.debug(`Key deletion status for '${key}': ${deleted}`);
|
||||
return deleted;
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error deleting key '${key}' from Infinispan.`, {
|
||||
error: error.message,
|
||||
});
|
||||
this.emit(
|
||||
'error',
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
this.logger.info('Clearing all entries from Infinispan cache.');
|
||||
try {
|
||||
const client = await this.getClient();
|
||||
await client.clear();
|
||||
this.logger.info('Infinispan cache cleared successfully.');
|
||||
} catch (error: any) {
|
||||
this.logger.error('Error clearing Infinispan cache.', {
|
||||
error: error.message,
|
||||
});
|
||||
this.emit(
|
||||
'error',
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// This disconnect is for the Keyv store instance, but the actual client is shared.
|
||||
// The CacheManager should handle the shared client's disconnection.
|
||||
// However, if Keyv calls this, we shouldn't error.
|
||||
async disconnect(): Promise<void> {
|
||||
this.logger.info(
|
||||
'InfinispanKeyvStore disconnect called. Shared client managed by CacheManager.',
|
||||
);
|
||||
// No-op for this store instance as the actual client is managed externally by CacheManager.
|
||||
// The CacheManager's stop() method will disconnect the shared native client.
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
Vendored
+225
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
LoggerService,
|
||||
RootConfigService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
InfinispanAuthOptions,
|
||||
InfinispanCacheStoreOptions,
|
||||
InfinispanClientAuthOptions,
|
||||
InfinispanClientBehaviorOptions,
|
||||
InfinispanClusterConfig,
|
||||
InfinispanServerConfig,
|
||||
InfinispanSslOptions,
|
||||
} from '../../types';
|
||||
|
||||
export class InfinispanOptionsMapper {
|
||||
/**
|
||||
* Parses Infinispan options from the provided configuration path.
|
||||
*
|
||||
* @param storeConfigPath - The configuration path for the Infinispan store.
|
||||
* @param config - The root configuration service to retrieve the Infinispan configuration.
|
||||
* @param logger - An optional logger service for logging errors and warnings.
|
||||
* @returns Parsed Infinispan cache store options.
|
||||
*/
|
||||
public static parseInfinispanOptions(
|
||||
storeConfigPath: string,
|
||||
config: RootConfigService,
|
||||
logger?: LoggerService,
|
||||
): InfinispanCacheStoreOptions {
|
||||
const infinispanConfig = config.getConfig(storeConfigPath);
|
||||
const parsedOptions: Partial<InfinispanCacheStoreOptions> = {
|
||||
type: 'infinispan',
|
||||
};
|
||||
|
||||
// Parse Servers & Clusters Configurations
|
||||
if (infinispanConfig.has('servers')) {
|
||||
const serversConfig = infinispanConfig.get('servers');
|
||||
if (Array.isArray(serversConfig)) {
|
||||
parsedOptions.servers = infinispanConfig.getConfigArray('servers').map(
|
||||
serverConf =>
|
||||
({
|
||||
host: serverConf.getString('host'),
|
||||
port: serverConf.getNumber('port'),
|
||||
} as InfinispanServerConfig),
|
||||
);
|
||||
} else if (typeof serversConfig === 'object' && serversConfig !== null) {
|
||||
const serverConf = infinispanConfig.getConfig('servers');
|
||||
parsedOptions.servers = {
|
||||
host: serverConf.getString('host'),
|
||||
port: serverConf.getNumber('port'),
|
||||
} as InfinispanServerConfig;
|
||||
} else {
|
||||
logger?.error(
|
||||
`Infinispan 'servers' configuration at ${storeConfigPath} must be an object or an array.`,
|
||||
);
|
||||
throw new Error(
|
||||
`Infinispan 'servers' configuration at ${storeConfigPath} is invalid.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger?.error(
|
||||
`Infinispan configuration at ${storeConfigPath} is missing the 'servers' definition.`,
|
||||
);
|
||||
throw new Error(
|
||||
`Infinispan configuration at ${storeConfigPath} must define 'servers'.`,
|
||||
);
|
||||
}
|
||||
|
||||
// The parsed options block to send to the Infinispan client
|
||||
// This will be used to configure the Infinispan client behavior.
|
||||
const behaviorOptions: Partial<InfinispanClientBehaviorOptions> = {};
|
||||
|
||||
if (infinispanConfig.has('clusters')) {
|
||||
const clustersConfig = infinispanConfig.getConfigArray('clusters');
|
||||
const clusterOptions: InfinispanClusterConfig[] = [];
|
||||
clustersConfig?.forEach(clusterConf => {
|
||||
const name = clusterConf.getOptionalString('name');
|
||||
const cluster: InfinispanClusterConfig = {
|
||||
...(name && { name }),
|
||||
servers: clusterConf.getConfigArray('servers').map(serverConf => ({
|
||||
host: serverConf.getString('host'),
|
||||
port: serverConf.getNumber('port'),
|
||||
})),
|
||||
};
|
||||
clusterOptions.push(cluster);
|
||||
behaviorOptions.clusters = clusterOptions;
|
||||
});
|
||||
}
|
||||
|
||||
// Parse Default Options Start...
|
||||
if (infinispanConfig.has('version')) {
|
||||
const clientVersion = infinispanConfig.getString('version');
|
||||
if (
|
||||
clientVersion === '2.9' ||
|
||||
clientVersion === '2.5' ||
|
||||
clientVersion === '2.2'
|
||||
) {
|
||||
behaviorOptions.version = clientVersion;
|
||||
} else if (clientVersion !== null && clientVersion !== undefined) {
|
||||
logger?.warn(
|
||||
`Invalid Infinispan client version "${clientVersion}" in config at ${storeConfigPath}.version. Must be "2.9", "2.5", or "2.2". It will be ignored, and the client may use a default or fail.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (infinispanConfig.has('cacheName')) {
|
||||
behaviorOptions.cacheName = infinispanConfig.getString('cacheName');
|
||||
}
|
||||
|
||||
const mediaType = infinispanConfig.getOptionalString('mediaType');
|
||||
if (mediaType === 'text/plain' || mediaType === 'application/json') {
|
||||
behaviorOptions.mediaType = mediaType;
|
||||
} else if (mediaType !== null && mediaType !== undefined) {
|
||||
logger?.warn(
|
||||
`Invalid Infinispan mediaType "${mediaType}" in config at ${storeConfigPath}.mediaType. Must be "text/plain" | "application/json". It will be ignored, and the client may use a default or fail.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (infinispanConfig.has('maxRetries')) {
|
||||
behaviorOptions.maxRetries = infinispanConfig.getNumber('maxRetries');
|
||||
}
|
||||
|
||||
if (infinispanConfig.has('topologyUpdates')) {
|
||||
behaviorOptions.topologyUpdates =
|
||||
infinispanConfig.getBoolean('topologyUpdates');
|
||||
}
|
||||
|
||||
// Default Options End...
|
||||
|
||||
// Parse Authentication and SSL Options
|
||||
if (infinispanConfig.has('authentication')) {
|
||||
const authConfig = infinispanConfig.getConfig('authentication');
|
||||
const auth: Partial<InfinispanAuthOptions> = {};
|
||||
|
||||
if (authConfig.has('enabled')) {
|
||||
auth.enabled = authConfig.getBoolean('enabled');
|
||||
}
|
||||
if (authConfig.has('saslMechanism')) {
|
||||
auth.saslMechanism = authConfig.getString('saslMechanism');
|
||||
}
|
||||
if (authConfig.has('userName')) {
|
||||
auth.userName = authConfig.getString('userName');
|
||||
}
|
||||
if (authConfig.has('password')) {
|
||||
auth.password = authConfig.getString('password');
|
||||
}
|
||||
if (authConfig.has('token')) {
|
||||
auth.token = authConfig.getString('token');
|
||||
}
|
||||
if (authConfig.has('authzid')) {
|
||||
auth.authzid = authConfig.getString('authzid');
|
||||
}
|
||||
|
||||
behaviorOptions.authentication = auth as InfinispanAuthOptions;
|
||||
}
|
||||
|
||||
if (infinispanConfig.has('ssl')) {
|
||||
const sslConfig = infinispanConfig.getConfig('ssl');
|
||||
const ssl: Partial<InfinispanSslOptions> = {};
|
||||
|
||||
if (sslConfig.has('enabled')) {
|
||||
ssl.enabled = sslConfig.getBoolean('enabled');
|
||||
}
|
||||
if (sslConfig.has('secureProtocol')) {
|
||||
ssl.secureProtocol = sslConfig.getString('secureProtocol');
|
||||
}
|
||||
if (sslConfig.has('trustCerts')) {
|
||||
ssl.trustCerts = sslConfig.getStringArray('trustCerts');
|
||||
}
|
||||
if (sslConfig.has('sniHostname')) {
|
||||
ssl.sniHostName = sslConfig.getString('sniHostname');
|
||||
}
|
||||
|
||||
if (sslConfig.has('clientAuth')) {
|
||||
const clientAuth = infinispanConfig.getConfig('clientAuth');
|
||||
const clientAuthOpts: Partial<InfinispanClientAuthOptions> = {};
|
||||
|
||||
if (clientAuth.has('key')) {
|
||||
clientAuthOpts.key = clientAuth.getString('key');
|
||||
}
|
||||
if (clientAuth.has('passphrase')) {
|
||||
clientAuthOpts.passphrase = clientAuth.getString('passphrase');
|
||||
}
|
||||
if (clientAuth.has('cert')) {
|
||||
clientAuthOpts.cert = clientAuth.getString('cert');
|
||||
}
|
||||
ssl.clientAuth = clientAuthOpts as InfinispanClientAuthOptions;
|
||||
}
|
||||
|
||||
if (sslConfig.has('cryptoStore')) {
|
||||
const cryptoStore = infinispanConfig.getConfig('cryptoStore');
|
||||
const cryptoStoreOpts: Partial<InfinispanClientAuthOptions> = {};
|
||||
|
||||
if (cryptoStore.has('path')) {
|
||||
cryptoStoreOpts.key = cryptoStore.getString('path');
|
||||
}
|
||||
if (cryptoStore.has('passphrase')) {
|
||||
cryptoStoreOpts.passphrase = cryptoStore.getString('passphrase');
|
||||
}
|
||||
ssl.cryptoStore = cryptoStoreOpts as InfinispanClientAuthOptions;
|
||||
}
|
||||
|
||||
behaviorOptions.ssl = ssl as InfinispanSslOptions;
|
||||
}
|
||||
|
||||
parsedOptions.options = behaviorOptions as InfinispanClientBehaviorOptions;
|
||||
|
||||
return parsedOptions as InfinispanCacheStoreOptions;
|
||||
}
|
||||
}
|
||||
+96
-1
@@ -24,6 +24,7 @@ import { RedisClusterOptions, KeyvRedisOptions } from '@keyv/redis';
|
||||
* @public
|
||||
*/
|
||||
export type RedisCacheStoreOptions = {
|
||||
type: 'redis' | 'valkey';
|
||||
client?: KeyvRedisOptions;
|
||||
cluster?: RedisClusterOptions;
|
||||
};
|
||||
@@ -33,7 +34,9 @@ export type RedisCacheStoreOptions = {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type CacheStoreOptions = RedisCacheStoreOptions;
|
||||
export type CacheStoreOptions =
|
||||
| RedisCacheStoreOptions
|
||||
| InfinispanCacheStoreOptions;
|
||||
|
||||
/**
|
||||
* Options given when constructing a {@link CacheManager}.
|
||||
@@ -56,3 +59,95 @@ export type CacheManagerOptions = {
|
||||
export function ttlToMilliseconds(ttl: number | HumanDuration): number {
|
||||
return typeof ttl === 'number' ? ttl : durationToMilliseconds(ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for a single Infinispan server.
|
||||
*/
|
||||
export type InfinispanServerConfig = {
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for putting values into Infinispan cache.
|
||||
*/
|
||||
export type InfinispanPutOptions = {
|
||||
lifespan?: string;
|
||||
maxIdle?: string;
|
||||
previous?: boolean;
|
||||
flags?: string[];
|
||||
};
|
||||
/**
|
||||
* SSL/TLS options for the Infinispan client.
|
||||
*/
|
||||
export type InfinispanSslOptions = {
|
||||
enabled: boolean;
|
||||
secureProtocol?: string;
|
||||
trustCerts?: string[]; // Array of trusted CA certificates
|
||||
clientAuth?: InfinispanClientAuthOptions;
|
||||
cryptoStore?: InfinispanCryptoStoreOptions;
|
||||
sniHostName?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Authentication options for the Infinispan client.
|
||||
* This is used for client-side authentication with the Infinispan server.
|
||||
*/
|
||||
export type InfinispanClientAuthOptions = {
|
||||
key?: string;
|
||||
passphrase?: string;
|
||||
cert?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for the Infinispan client crypto store.
|
||||
* This is used for storing keys and certificates securely.
|
||||
*/
|
||||
export type InfinispanCryptoStoreOptions = {
|
||||
path?: string;
|
||||
passphrase?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Authentication options for the Infinispan client.
|
||||
*/
|
||||
export type InfinispanAuthOptions = {
|
||||
enabled: boolean;
|
||||
saslMechanism?: string;
|
||||
userName?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
authzid?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for the Infinispan cache store, designed to be configured
|
||||
* in app-config.yaml under `backend.cache.infinispan`.
|
||||
*/
|
||||
export type InfinispanCacheStoreOptions = {
|
||||
type: 'infinispan';
|
||||
servers: InfinispanServerConfig | InfinispanServerConfig[];
|
||||
options?: InfinispanClientBehaviorOptions;
|
||||
};
|
||||
|
||||
export type InfinispanClusterConfig = {
|
||||
name?: string;
|
||||
servers: InfinispanServerConfig[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Detailed client behavior options for the Infinispan client.
|
||||
* @public
|
||||
*/
|
||||
export type InfinispanClientBehaviorOptions = {
|
||||
version?: '2.9' | '2.5' | '2.2';
|
||||
cacheName?: string;
|
||||
maxRetries?: number;
|
||||
connectionTimeout?: number;
|
||||
socketTimeout?: number;
|
||||
authentication?: InfinispanAuthOptions;
|
||||
ssl?: InfinispanSslOptions;
|
||||
mediaType?: 'text/plain' | 'application/json';
|
||||
topologyUpdates?: boolean;
|
||||
clusters?: InfinispanClusterConfig[];
|
||||
};
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"cookie": "^0.7.0",
|
||||
"express": "^4.17.1",
|
||||
"fs-extra": "^11.0.0",
|
||||
"infinispan": "^0.12.0",
|
||||
"keyv": "^5.2.1",
|
||||
"knex": "^3.0.0",
|
||||
"mysql2": "^3.0.0",
|
||||
|
||||
@@ -2588,6 +2588,7 @@ __metadata:
|
||||
git-url-parse: "npm:^15.0.0"
|
||||
helmet: "npm:^6.0.0"
|
||||
http-errors: "npm:^2.0.0"
|
||||
infinispan: "npm:^0.12.0"
|
||||
is-glob: "npm:^4.0.3"
|
||||
jose: "npm:^5.0.0"
|
||||
keyv: "npm:^5.2.1"
|
||||
@@ -2755,6 +2756,7 @@ __metadata:
|
||||
cookie: "npm:^0.7.0"
|
||||
express: "npm:^4.17.1"
|
||||
fs-extra: "npm:^11.0.0"
|
||||
infinispan: "npm:^0.12.0"
|
||||
keyv: "npm:^5.2.1"
|
||||
knex: "npm:^3.0.0"
|
||||
mysql2: "npm:^3.0.0"
|
||||
@@ -25290,6 +25292,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer-xor@npm:^2.0.2":
|
||||
version: 2.0.2
|
||||
resolution: "buffer-xor@npm:2.0.2"
|
||||
dependencies:
|
||||
safe-buffer: "npm:^5.1.1"
|
||||
checksum: 10/78226fcae9f4a0b4adec69dffc049f26f6bab240dfdd1b3f6fe07c4eb6b90da202ea5c363f98af676156ee39450a06405fddd9e8965f68a5327edcc89dcbe5d0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer@npm:5.6.0":
|
||||
version: 5.6.0
|
||||
resolution: "buffer@npm:5.6.0"
|
||||
@@ -27845,6 +27856,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"default-user-agent@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "default-user-agent@npm:1.0.0"
|
||||
dependencies:
|
||||
os-name: "npm:~1.0.3"
|
||||
checksum: 10/b1ef07c8e7de846a66e1e120d7ba11969faa36c8db4af2317f9b64d30e7507d129e3f721c7cc3f531a1719c1ab463d830bf426fbcda87b11defe23689f4d2b60
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"defaults@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "defaults@npm:1.0.3"
|
||||
@@ -28250,6 +28270,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"digest-header@npm:^1.0.0":
|
||||
version: 1.1.0
|
||||
resolution: "digest-header@npm:1.1.0"
|
||||
checksum: 10/fadbdda75e1cc650e460c8fe2064f74c43cc005d0eab66cc390dd1ae2678cfb41f69f151323fbd3e059e28c941f1b9adc6ea4dbd9c918cb246f34a5eb8e103f0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dir-glob@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "dir-glob@npm:3.0.1"
|
||||
@@ -31150,6 +31177,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"form-data-encoder@npm:^1.7.2":
|
||||
version: 1.9.0
|
||||
resolution: "form-data-encoder@npm:1.9.0"
|
||||
checksum: 10/d6a684f22660e4857ef846ad8154c00c0f0e174f3edca24567ab93d9d5b5d765b2951518672db1fccc5e1f91d66bb0d9a54f99dbd9b915d204bc6887c6a0084c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"form-data@npm:^2.3.2, form-data@npm:^2.5.0":
|
||||
version: 2.5.1
|
||||
resolution: "form-data@npm:2.5.1"
|
||||
@@ -31181,7 +31215,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"formdata-node@npm:^4.3.2":
|
||||
"formdata-node@npm:^4.3.2, formdata-node@npm:^4.3.3":
|
||||
version: 4.4.1
|
||||
resolution: "formdata-node@npm:4.4.1"
|
||||
dependencies:
|
||||
@@ -31211,6 +31245,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"formstream@npm:^1.1.1":
|
||||
version: 1.5.2
|
||||
resolution: "formstream@npm:1.5.2"
|
||||
dependencies:
|
||||
destroy: "npm:^1.0.4"
|
||||
mime: "npm:^2.5.2"
|
||||
node-hex: "npm:^1.0.1"
|
||||
pause-stream: "npm:~0.0.11"
|
||||
checksum: 10/d2892fa4756260733db1f7626d3845d9c2294625d3a709bd2eb3a1899e0f17ae38bd2ad3dfd1e1d11d25f1a2789869523b6a9ec8adc9d8f5b3c0d33e58f3d4c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"forwarded-parse@npm:2.1.2":
|
||||
version: 2.1.2
|
||||
resolution: "forwarded-parse@npm:2.1.2"
|
||||
@@ -33227,6 +33273,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"infinispan@npm:^0.12.0":
|
||||
version: 0.12.0
|
||||
resolution: "infinispan@npm:0.12.0"
|
||||
dependencies:
|
||||
buffer-xor: "npm:^2.0.2"
|
||||
log4js: "npm:^6.4.6"
|
||||
protobufjs: "npm:^7.0.0"
|
||||
underscore: "npm:^1.13.3"
|
||||
urllib: "npm:^3.23.0"
|
||||
checksum: 10/e805772da3304b088293457bf766749f3a6574428bd8724f234a1ceb95bdbbebb36fef6154740da3b21c62a12d8b73b1ff666ce3dc8a18648f77d9416c63e0ae
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"inflight@npm:^1.0.4":
|
||||
version: 1.0.6
|
||||
resolution: "inflight@npm:1.0.6"
|
||||
@@ -36802,7 +36861,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"log4js@npm:6.9.1":
|
||||
"log4js@npm:6.9.1, log4js@npm:^6.4.6":
|
||||
version: 6.9.1
|
||||
resolution: "log4js@npm:6.9.1"
|
||||
dependencies:
|
||||
@@ -38037,7 +38096,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:^2.1.18, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34":
|
||||
"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:^2.1.18, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:^2.1.35, mime-types@npm:~2.1.17, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34":
|
||||
version: 2.1.35
|
||||
resolution: "mime-types@npm:2.1.35"
|
||||
dependencies:
|
||||
@@ -38236,7 +38295,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8":
|
||||
"minimist@npm:^1.1.0, minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8":
|
||||
version: 1.2.8
|
||||
resolution: "minimist@npm:1.2.8"
|
||||
checksum: 10/908491b6cc15a6c440ba5b22780a0ba89b9810e1aea684e253e43c4e3b8d56ec1dcdd7ea96dde119c29df59c936cde16062159eae4225c691e19c70b432b6e6f
|
||||
@@ -39183,6 +39242,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-hex@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "node-hex@npm:1.0.1"
|
||||
checksum: 10/9053d532859ee7e9653972af77ac7b73edc4f13b9b53d0b96e4045e3ac78ac4460571d4b72ad31e9095be5f7d01e6fd71f268f02ad6029091f8cabae1d4ce4df
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-html-markdown@npm:^1.3.0":
|
||||
version: 1.3.0
|
||||
resolution: "node-html-markdown@npm:1.3.0"
|
||||
@@ -40111,6 +40177,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"os-name@npm:~1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "os-name@npm:1.0.3"
|
||||
dependencies:
|
||||
osx-release: "npm:^1.0.0"
|
||||
win-release: "npm:^1.0.0"
|
||||
bin:
|
||||
os-name: cli.js
|
||||
checksum: 10/2fc86cc199f8b4992bb00041401c5ab0407e3069e05981f3aa3e5a44cee9b7f22c2b0f5db2c0c1d55656c519884272b5e1e55517358c2e5f728b37dd38f5af78
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"os-tmpdir@npm:~1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "os-tmpdir@npm:1.0.2"
|
||||
@@ -40118,6 +40196,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"osx-release@npm:^1.0.0":
|
||||
version: 1.1.0
|
||||
resolution: "osx-release@npm:1.1.0"
|
||||
dependencies:
|
||||
minimist: "npm:^1.1.0"
|
||||
bin:
|
||||
osx-release: cli.js
|
||||
checksum: 10/48f442f836e514d08ce73ef786db8d7cf0958e8c64a04548767ddf1081454e323fa3b7b83dcf084ecf70fe304f484e6dab0fe33e80459ac0cf7d15c1bbbe9243
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"outdent@npm:^0.5.0":
|
||||
version: 0.5.0
|
||||
resolution: "outdent@npm:0.5.0"
|
||||
@@ -40848,6 +40937,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pause-stream@npm:~0.0.11":
|
||||
version: 0.0.11
|
||||
resolution: "pause-stream@npm:0.0.11"
|
||||
dependencies:
|
||||
through: "npm:~2.3"
|
||||
checksum: 10/1407efadfe814b5c487e4b28d6139cb7e03ee5d25fbb5f89a68f2053e81f05ce6b2bec196eeb3d46ef2c856f785016d14816b0d0e3c3abd1b64311c5c20660dc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pause@npm:0.0.1":
|
||||
version: 0.0.1
|
||||
resolution: "pause@npm:0.0.1"
|
||||
@@ -42136,9 +42234,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"protobufjs@npm:^7.2.5, protobufjs@npm:^7.2.6, protobufjs@npm:^7.3.0, protobufjs@npm:^7.3.2, protobufjs@npm:^7.4.0":
|
||||
version: 7.4.0
|
||||
resolution: "protobufjs@npm:7.4.0"
|
||||
"protobufjs@npm:^7.0.0, protobufjs@npm:^7.2.5, protobufjs@npm:^7.2.6, protobufjs@npm:^7.3.0, protobufjs@npm:^7.3.2, protobufjs@npm:^7.4.0":
|
||||
version: 7.5.3
|
||||
resolution: "protobufjs@npm:7.5.3"
|
||||
dependencies:
|
||||
"@protobufjs/aspromise": "npm:^1.1.2"
|
||||
"@protobufjs/base64": "npm:^1.1.2"
|
||||
@@ -42152,7 +42250,7 @@ __metadata:
|
||||
"@protobufjs/utf8": "npm:^1.1.0"
|
||||
"@types/node": "npm:>=13.7.0"
|
||||
long: "npm:^5.0.0"
|
||||
checksum: 10/408423506610f70858d7593632f4a6aa4f05796c90fd632be9b9252457c795acc71aa6d3b54bb7f48a890141728fee4ca3906723ccea6c202ad71f21b3879b8b
|
||||
checksum: 10/3e412d2e2f799875dcdac1b43508f465a499dd3ffd9d24073669702589ef016529905617a12a967b1a8cfbe803a638b494cc3ed8db035605c7570d1a7fc094c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -44819,7 +44917,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"semver@npm:^5.5.0, semver@npm:^5.6.0":
|
||||
"semver@npm:^5.0.1, semver@npm:^5.5.0, semver@npm:^5.6.0":
|
||||
version: 5.7.2
|
||||
resolution: "semver@npm:5.7.2"
|
||||
bin:
|
||||
@@ -47047,7 +47145,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"through@npm:2, through@npm:^2.3.6":
|
||||
"through@npm:2, through@npm:^2.3.6, through@npm:~2.3":
|
||||
version: 2.3.8
|
||||
resolution: "through@npm:2.3.8"
|
||||
checksum: 10/5da78346f70139a7d213b65a0106f3c398d6bc5301f9248b5275f420abc2c4b1e77c2abc72d218dedc28c41efb2e7c312cb76a7730d04f9c2d37d247da3f4198
|
||||
@@ -47734,10 +47832,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-fest@npm:^4.26.1":
|
||||
version: 4.26.1
|
||||
resolution: "type-fest@npm:4.26.1"
|
||||
checksum: 10/b82676194f80af228cb852e320d2ea8381c89d667d2e4d9f2bdfc8f254bccc039c7741a90c53617a4de0c9fdca8265ed18eb0888cd628f391c5c381c33a9f94b
|
||||
"type-fest@npm:^4.26.1, type-fest@npm:^4.3.1":
|
||||
version: 4.41.0
|
||||
resolution: "type-fest@npm:4.41.0"
|
||||
checksum: 10/617ace794ac0893c2986912d28b3065ad1afb484cad59297835a0807dc63286c39e8675d65f7de08fafa339afcb8fe06a36e9a188b9857756ae1e92ee8bda212
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -48084,7 +48182,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"underscore@npm:^1.12.1":
|
||||
"underscore@npm:^1.12.1, underscore@npm:^1.13.3":
|
||||
version: 1.13.7
|
||||
resolution: "underscore@npm:1.13.7"
|
||||
checksum: 10/1ce3368dbe73d1e99678fa5d341a9682bd27316032ad2de7883901918f0f5d50e80320ccc543f53c1862ab057a818abc560462b5f83578afe2dd8dd7f779766c
|
||||
@@ -48112,7 +48210,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undici@npm:^5.28.4":
|
||||
"undici@npm:^5.28.2, undici@npm:^5.28.4":
|
||||
version: 5.29.0
|
||||
resolution: "undici@npm:5.29.0"
|
||||
dependencies:
|
||||
@@ -48492,6 +48590,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"urllib@npm:^3.23.0":
|
||||
version: 3.27.3
|
||||
resolution: "urllib@npm:3.27.3"
|
||||
dependencies:
|
||||
default-user-agent: "npm:^1.0.0"
|
||||
digest-header: "npm:^1.0.0"
|
||||
form-data-encoder: "npm:^1.7.2"
|
||||
formdata-node: "npm:^4.3.3"
|
||||
formstream: "npm:^1.1.1"
|
||||
mime-types: "npm:^2.1.35"
|
||||
pump: "npm:^3.0.0"
|
||||
qs: "npm:^6.11.2"
|
||||
type-fest: "npm:^4.3.1"
|
||||
undici: "npm:^5.28.2"
|
||||
ylru: "npm:^1.3.2"
|
||||
checksum: 10/4d0a5a7afa8ae9697a573f643851f9508cf5c5a1e7d800830870740d0561bddf3599861f228b119e95d870b1dae5fe2e1183c0840ebec001fc93e9fd0b8e1701
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"urlpattern-polyfill@npm:^8.0.0":
|
||||
version: 8.0.2
|
||||
resolution: "urlpattern-polyfill@npm:8.0.2"
|
||||
@@ -49396,6 +49513,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"win-release@npm:^1.0.0":
|
||||
version: 1.1.1
|
||||
resolution: "win-release@npm:1.1.1"
|
||||
dependencies:
|
||||
semver: "npm:^5.0.1"
|
||||
checksum: 10/8943898cc4badaf8598342d63093e49ae9a64c140cf190e81472d3a8890f3387b8408181412e1b58658fe7777ce5d1e3f02eee4beeaee49909d1d17a72d52fc1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"winston-transport@npm:^4.5.0, winston-transport@npm:^4.7.0":
|
||||
version: 4.8.0
|
||||
resolution: "winston-transport@npm:4.8.0"
|
||||
@@ -49815,7 +49941,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ylru@npm:^1.2.0":
|
||||
"ylru@npm:^1.2.0, ylru@npm:^1.3.2":
|
||||
version: 1.4.0
|
||||
resolution: "ylru@npm:1.4.0"
|
||||
checksum: 10/5437f8eb2fb5dd515845c657dde3cecaa9f6bd4c6386d2a5212d3fafe02189c7d8ebfdfc84940a7811607cb3524eb362ce95d3180d355cd5deb610aa8c82c9bc
|
||||
|
||||
Reference in New Issue
Block a user