@tmlmobilidade/databases 20260506.1355.7 → 20260507.11.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ import { type RedisClientType } from 'redis';
2
+ export declare class GORedisClient {
3
+ private static _instance;
4
+ private client;
5
+ private tunnel;
6
+ /**
7
+ * Disallow direct instantiation of the service.
8
+ * Use getClient() instead to ensure singleton behavior.
9
+ */
10
+ private constructor();
11
+ /**
12
+ * Returns the singleton instance of the subclass.
13
+ */
14
+ static getClient(): Promise<RedisClientType>;
15
+ /**
16
+ * Connects to Redis, setting up the client instance.
17
+ * If SSH tunneling is required, it establishes the tunnel first.
18
+ * This method is called internally by the service and should not be used directly.
19
+ */
20
+ private connect;
21
+ /**
22
+ * Constructs the connection string based on environment variables
23
+ * and SSH tunneling configuration, and handles both direct connections and SSH-tunneled
24
+ * connections, validating the necessary environment variables for each case.
25
+ * This method is called internally by the service and should not be used directly.
26
+ * @throws Will throw an error if required environment variables are missing or if the SSH tunnel setup fails.
27
+ * @returns A promise that resolves to the Redis connection string.
28
+ */
29
+ private getConnectionString;
30
+ }
@@ -0,0 +1,114 @@
1
+ /* * */
2
+ import { Logger } from '@tmlmobilidade/logger';
3
+ import { SshTunnelService } from '@tmlmobilidade/ssh';
4
+ import { readFileSync } from 'node:fs';
5
+ import { createClient } from 'redis';
6
+ /* * */
7
+ export class GORedisClient {
8
+ //
9
+ static _instance = null;
10
+ client;
11
+ tunnel = null;
12
+ /**
13
+ * Disallow direct instantiation of the service.
14
+ * Use getClient() instead to ensure singleton behavior.
15
+ */
16
+ constructor() { }
17
+ /**
18
+ * Returns the singleton instance of the subclass.
19
+ */
20
+ static async getClient() {
21
+ // If no instance exists, create one and store the promise.
22
+ // This ensures that if multiple calls to getClient() happen concurrently,
23
+ // they will all await the same initialization process.
24
+ if (!this._instance) {
25
+ this._instance = (async () => {
26
+ const instance = new GORedisClient();
27
+ // This behaves like the constructor,
28
+ // but allows for async initialization.
29
+ await instance.connect();
30
+ return instance;
31
+ })();
32
+ }
33
+ // Await the instance if it's still initializing,
34
+ // or return it immediately if ready.
35
+ const instance = await this._instance;
36
+ return instance.client;
37
+ }
38
+ /**
39
+ * Connects to Redis, setting up the client instance.
40
+ * If SSH tunneling is required, it establishes the tunnel first.
41
+ * This method is called internally by the service and should not be used directly.
42
+ */
43
+ async connect() {
44
+ Logger.info('[GORedisClient] Connecting to database...');
45
+ const connectionString = await this.getConnectionString();
46
+ this.client = createClient({ url: connectionString });
47
+ await this.client.connect();
48
+ Logger.info('[GORedisClient] Connected to database');
49
+ }
50
+ /**
51
+ * Constructs the connection string based on environment variables
52
+ * and SSH tunneling configuration, and handles both direct connections and SSH-tunneled
53
+ * connections, validating the necessary environment variables for each case.
54
+ * This method is called internally by the service and should not be used directly.
55
+ * @throws Will throw an error if required environment variables are missing or if the SSH tunnel setup fails.
56
+ * @returns A promise that resolves to the Redis connection string.
57
+ */
58
+ async getConnectionString() {
59
+ //
60
+ //
61
+ // Validate required environment variables
62
+ if (process.env.GO_REDIS_TUNNEL_ENABLED !== 'true' && process.env.GO_REDIS_TUNNEL_ENABLED !== 'false') {
63
+ throw new Error('Missing GO_REDIS_TUNNEL_ENABLED. Please indicate whether SSH tunneling is required by setting GO_REDIS_TUNNEL_ENABLED to "true" or "false".');
64
+ }
65
+ if (!process.env.GO_REDIS_HOST || !process.env.GO_REDIS_PORT) {
66
+ throw new Error('Missing GO_REDIS_HOST or GO_REDIS_PORT');
67
+ }
68
+ if (process.env.GO_REDIS_TUNNEL_ENABLED === 'false') {
69
+ return `redis://${process.env.GO_REDIS_HOST}:${process.env.GO_REDIS_PORT}`;
70
+ }
71
+ // SSH required
72
+ if (!process.env.GO_REDIS_TUNNEL_LOCAL_PORT) {
73
+ throw new Error('Missing GO_REDIS_TUNNEL_LOCAL_PORT');
74
+ }
75
+ if (!process.env.GO_REDIS_TUNNEL_SSH_HOST || !process.env.GO_REDIS_TUNNEL_SSH_USERNAME) {
76
+ throw new Error('Missing SSH config');
77
+ }
78
+ const sshConfig = {
79
+ forwardOptions: {
80
+ dstAddr: process.env.GO_REDIS_HOST,
81
+ dstPort: Number(process.env.GO_REDIS_PORT),
82
+ srcAddr: 'localhost',
83
+ srcPort: Number(process.env.GO_REDIS_TUNNEL_LOCAL_PORT),
84
+ },
85
+ serverOptions: {
86
+ port: Number(process.env.GO_REDIS_TUNNEL_LOCAL_PORT),
87
+ },
88
+ sshOptions: {
89
+ agent: process.env.GO_REDIS_TUNNEL_SSH_KEY_PATH ? undefined : process.env.SSH_AUTH_SOCK,
90
+ host: process.env.GO_REDIS_TUNNEL_SSH_HOST,
91
+ keepaliveCountMax: 20,
92
+ keepaliveInterval: 10_000,
93
+ port: 22,
94
+ privateKey: process.env.GO_REDIS_TUNNEL_SSH_KEY_PATH ? readFileSync(process.env.GO_REDIS_TUNNEL_SSH_KEY_PATH) : undefined,
95
+ username: process.env.GO_REDIS_TUNNEL_SSH_USERNAME,
96
+ },
97
+ tunnelOptions: {
98
+ autoClose: false,
99
+ reconnectOnError: true,
100
+ },
101
+ };
102
+ const sshOptions = {
103
+ maxRetries: 3,
104
+ };
105
+ this.tunnel = new SshTunnelService(sshConfig, sshOptions);
106
+ Logger.info('[GORedisClient] Setting up SSH Tunnel...');
107
+ const connection = await this.tunnel.connect();
108
+ const addr = connection.address();
109
+ if (!addr || typeof addr !== 'object') {
110
+ throw new Error('[GORedisClient] Failed to retrieve SSH tunnel address.');
111
+ }
112
+ return `redis://localhost:${addr.port}`;
113
+ }
114
+ }
@@ -0,0 +1,2 @@
1
+ export * from './interface.js';
2
+ export * from './keys.js';
@@ -0,0 +1,2 @@
1
+ export * from './interface.js';
2
+ export * from './keys.js';
@@ -0,0 +1,50 @@
1
+ import { type ApiCacheKey } from './keys.js';
2
+ import { type RedisClientType } from 'redis';
3
+ declare class ApiCacheClass {
4
+ private static _instance;
5
+ private client;
6
+ /**
7
+ * Returns the singleton instance of the subclass.
8
+ */
9
+ static getInstance(): Promise<ApiCacheClass>;
10
+ /**
11
+ * Deletes all keys from the cache that are not defined in `ApiCacheKeyValues`.
12
+ * This method is useful for maintaining a clean state free of stale
13
+ * or irrelevant cache entries that consume storage and memory resources.
14
+ * @returns A promise that resolves when the cleaning process is complete.
15
+ * @throws Will throw an error if the cleaning process fails.
16
+ */
17
+ clean(): Promise<void>;
18
+ /**
19
+ * Deletes a cache entry by its key.
20
+ * @param key The key of the cache entry to delete.
21
+ * @returns A promise that resolves when the deletion process is complete.
22
+ * @throws Will throw an error if the deletion process fails.
23
+ */
24
+ delete(key: ApiCacheKey): Promise<void>;
25
+ /**
26
+ * Retrieves a cache entry by its key.
27
+ * @param key The key of the cache entry to retrieve.
28
+ * @returns A promise that resolves with the cache entry value,
29
+ * or `null` if not found.
30
+ * @throws Will throw an error if the retrieval process fails.
31
+ */
32
+ get(key: ApiCacheKey): Promise<null | string>;
33
+ /**
34
+ * Saves a cache entry with an optional time-to-live (TTL).
35
+ * @param key The key of the cache entry to save.
36
+ * @param value The value of the cache entry to save. Must be a string.
37
+ * @param ttl Optional time-to-live for the cache entry in seconds.
38
+ * If not provided, the entry will persist indefinitely.
39
+ */
40
+ set(key: ApiCacheKey, value: string, ttl?: number): Promise<void>;
41
+ protected connectToClient(): Promise<RedisClientType>;
42
+ /**
43
+ * Initializes the Redis client.
44
+ * @throws Will throw an error if the client initialization fails.
45
+ * @returns A promise that resolves when the initialization process is complete.
46
+ */
47
+ protected init(): Promise<void>;
48
+ }
49
+ export declare const apiCache: ApiCacheClass;
50
+ export {};
@@ -0,0 +1,102 @@
1
+ /* * */
2
+ import { GORedisClient } from '../../clients/go-redis.js';
3
+ import { ApiCacheKeyValues } from './keys.js';
4
+ import { asyncSingletonProxy } from '@tmlmobilidade/utils';
5
+ /* * */
6
+ class ApiCacheClass {
7
+ //
8
+ static _instance = null;
9
+ client;
10
+ /**
11
+ * Returns the singleton instance of the subclass.
12
+ */
13
+ static async getInstance() {
14
+ // If no instance exists, create one and store the promise.
15
+ // This ensures that if multiple calls to getInstance() happen concurrently,
16
+ // they will all await the same initialization process.
17
+ if (!this._instance) {
18
+ this._instance = (async () => {
19
+ const instance = new ApiCacheClass();
20
+ // This behaves like the constructor,
21
+ // but allows for async initialization.
22
+ await instance.init();
23
+ return instance;
24
+ })();
25
+ }
26
+ // Await the instance if it's still initializing,
27
+ // or return it immediately if ready.
28
+ return await this._instance;
29
+ }
30
+ /**
31
+ * Deletes all keys from the cache that are not defined in `ApiCacheKeyValues`.
32
+ * This method is useful for maintaining a clean state free of stale
33
+ * or irrelevant cache entries that consume storage and memory resources.
34
+ * @returns A promise that resolves when the cleaning process is complete.
35
+ * @throws Will throw an error if the cleaning process fails.
36
+ */
37
+ async clean() {
38
+ const allKeys = await this.client.keys('*');
39
+ const keysToDelete = allKeys.filter(key => !ApiCacheKeyValues.includes(key));
40
+ if (keysToDelete.length)
41
+ await this.client.del(keysToDelete);
42
+ }
43
+ /**
44
+ * Deletes a cache entry by its key.
45
+ * @param key The key of the cache entry to delete.
46
+ * @returns A promise that resolves when the deletion process is complete.
47
+ * @throws Will throw an error if the deletion process fails.
48
+ */
49
+ async delete(key) {
50
+ await this.client.del(key);
51
+ }
52
+ /**
53
+ * Retrieves a cache entry by its key.
54
+ * @param key The key of the cache entry to retrieve.
55
+ * @returns A promise that resolves with the cache entry value,
56
+ * or `null` if not found.
57
+ * @throws Will throw an error if the retrieval process fails.
58
+ */
59
+ async get(key) {
60
+ const result = await this.client.get(key);
61
+ if (typeof result !== 'string')
62
+ return null;
63
+ return result;
64
+ }
65
+ /**
66
+ * Saves a cache entry with an optional time-to-live (TTL).
67
+ * @param key The key of the cache entry to save.
68
+ * @param value The value of the cache entry to save. Must be a string.
69
+ * @param ttl Optional time-to-live for the cache entry in seconds.
70
+ * If not provided, the entry will persist indefinitely.
71
+ */
72
+ async set(key, value, ttl) {
73
+ // Validate value type before setting cache
74
+ if (typeof value !== 'string')
75
+ throw new Error(`[ApiCache] Value must be a string. Got "${typeof value}" for key "${key}".`);
76
+ // Check if key is valid before setting cache
77
+ if (!ApiCacheKeyValues.includes(key))
78
+ throw new Error(`[ApiCache] Invalid cache key "${key}". Allowed keys are: ${ApiCacheKeyValues.join(', ')}.`);
79
+ // Set cache with optional TTL
80
+ if (ttl)
81
+ await this.client.set(key, value, { expiration: { type: 'EX', value: ttl } });
82
+ else
83
+ await this.client.set(key, value);
84
+ }
85
+ connectToClient() {
86
+ return GORedisClient.getClient();
87
+ }
88
+ /**
89
+ * Initializes the Redis client.
90
+ * @throws Will throw an error if the client initialization fails.
91
+ * @returns A promise that resolves when the initialization process is complete.
92
+ */
93
+ async init() {
94
+ // Skip if already initialized
95
+ if (this.client)
96
+ return;
97
+ // Connect to the Redis client
98
+ this.client = await this.connectToClient();
99
+ }
100
+ }
101
+ /* * */
102
+ export const apiCache = asyncSingletonProxy(ApiCacheClass);
@@ -0,0 +1,2 @@
1
+ export declare const ApiCacheKeyValues: readonly ["alerts:all"];
2
+ export type ApiCacheKey = typeof ApiCacheKeyValues[number];
@@ -0,0 +1,4 @@
1
+ /* * */
2
+ export const ApiCacheKeyValues = [
3
+ 'alerts:all',
4
+ ];
@@ -1,3 +1,4 @@
1
1
  export * from './apex/index.js';
2
+ export * from './api-cache/index.js';
2
3
  export * from './simplified-apex/index.js';
3
4
  export * from './vehicle-events/index.js';
@@ -1,3 +1,4 @@
1
1
  export * from './apex/index.js';
2
+ export * from './api-cache/index.js';
2
3
  export * from './simplified-apex/index.js';
3
4
  export * from './vehicle-events/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmlmobilidade/databases",
3
- "version": "20260506.1355.7",
3
+ "version": "20260507.11.20",
4
4
  "author": {
5
5
  "email": "iso@tmlmobilidade.pt",
6
6
  "name": "TML-ISO"
@@ -42,6 +42,7 @@
42
42
  "@tmlmobilidade/types": "*",
43
43
  "@tmlmobilidade/utils": "*",
44
44
  "mongodb": "7.2.0",
45
+ "redis": "5.12.1",
45
46
  "zod": "3.25.76"
46
47
  },
47
48
  "devDependencies": {