@qwickapps/server 1.1.6 → 1.1.7
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.
- package/README.md +1 -1
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +5 -8
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/gateway.d.ts.map +1 -1
- package/dist/core/gateway.js +11 -23
- package/dist/core/gateway.js.map +1 -1
- package/dist/core/health-manager.d.ts.map +1 -1
- package/dist/core/health-manager.js +3 -9
- package/dist/core/health-manager.js.map +1 -1
- package/dist/core/logging.d.ts.map +1 -1
- package/dist/core/logging.js +1 -5
- package/dist/core/logging.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/cache-plugin.d.ts +219 -0
- package/dist/plugins/cache-plugin.d.ts.map +1 -0
- package/dist/plugins/cache-plugin.js +326 -0
- package/dist/plugins/cache-plugin.js.map +1 -0
- package/dist/plugins/cache-plugin.test.d.ts +8 -0
- package/dist/plugins/cache-plugin.test.d.ts.map +1 -0
- package/dist/plugins/cache-plugin.test.js +188 -0
- package/dist/plugins/cache-plugin.test.js.map +1 -0
- package/dist/plugins/config-plugin.js +1 -1
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/plugins/diagnostics-plugin.js +1 -1
- package/dist/plugins/diagnostics-plugin.js.map +1 -1
- package/dist/plugins/health-plugin.js +1 -1
- package/dist/plugins/health-plugin.js.map +1 -1
- package/dist/plugins/index.d.ts +6 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +4 -0
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/logs-plugin.d.ts.map +1 -1
- package/dist/plugins/logs-plugin.js +1 -3
- package/dist/plugins/logs-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.d.ts +155 -0
- package/dist/plugins/postgres-plugin.d.ts.map +1 -0
- package/dist/plugins/postgres-plugin.js +244 -0
- package/dist/plugins/postgres-plugin.js.map +1 -0
- package/dist/plugins/postgres-plugin.test.d.ts +8 -0
- package/dist/plugins/postgres-plugin.test.d.ts.map +1 -0
- package/dist/plugins/postgres-plugin.test.js +165 -0
- package/dist/plugins/postgres-plugin.test.js.map +1 -0
- package/dist-ui/assets/{index-Bk7ypbI4.js → index-CW1BviRn.js} +2 -2
- package/dist-ui/assets/{index-Bk7ypbI4.js.map → index-CW1BviRn.js.map} +1 -1
- package/dist-ui/index.html +1 -1
- package/package.json +13 -2
- package/src/core/control-panel.ts +5 -8
- package/src/core/gateway.ts +12 -24
- package/src/core/health-manager.ts +3 -9
- package/src/core/logging.ts +1 -5
- package/src/index.ts +22 -0
- package/src/plugins/cache-plugin.test.ts +241 -0
- package/src/plugins/cache-plugin.ts +503 -0
- package/src/plugins/config-plugin.ts +1 -1
- package/src/plugins/diagnostics-plugin.ts +1 -1
- package/src/plugins/health-plugin.ts +1 -1
- package/src/plugins/index.ts +10 -0
- package/src/plugins/logs-plugin.ts +1 -3
- package/src/plugins/postgres-plugin.test.ts +213 -0
- package/src/plugins/postgres-plugin.ts +345 -0
- package/ui/src/api/controlPanelApi.ts +1 -1
- package/ui/src/pages/LogsPage.tsx +6 -10
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Plugin
|
|
3
|
+
*
|
|
4
|
+
* Provides Redis caching capabilities with connection pooling and health checks.
|
|
5
|
+
* Wraps the 'ioredis' library with a simple, reusable interface.
|
|
6
|
+
*
|
|
7
|
+
* ## Features
|
|
8
|
+
* - Connection management with automatic reconnection
|
|
9
|
+
* - Key prefixing for multi-tenant/multi-app scenarios
|
|
10
|
+
* - TTL-based caching with setex/get operations
|
|
11
|
+
* - Automatic health checks
|
|
12
|
+
* - Multiple named instances support
|
|
13
|
+
* - Graceful shutdown
|
|
14
|
+
*
|
|
15
|
+
* ## Usage
|
|
16
|
+
*
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import { createGateway, createCachePlugin, getCache } from '@qwickapps/server';
|
|
19
|
+
*
|
|
20
|
+
* const gateway = createGateway({
|
|
21
|
+
* // ... config
|
|
22
|
+
* plugins: [
|
|
23
|
+
* createCachePlugin({
|
|
24
|
+
* url: process.env.REDIS_URL,
|
|
25
|
+
* keyPrefix: 'myapp:',
|
|
26
|
+
* }),
|
|
27
|
+
* ],
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // In your service code:
|
|
31
|
+
* const cache = getCache();
|
|
32
|
+
* await cache.set('user:123', userData, 3600); // Cache for 1 hour
|
|
33
|
+
* const user = await cache.get<User>('user:123');
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* ## Multiple Caches
|
|
37
|
+
*
|
|
38
|
+
* ```typescript
|
|
39
|
+
* // Register multiple caches with different names
|
|
40
|
+
* createCachePlugin({ url: primaryUrl, keyPrefix: 'session:' }, 'sessions');
|
|
41
|
+
* createCachePlugin({ url: cacheUrl, keyPrefix: 'cache:' }, 'content');
|
|
42
|
+
*
|
|
43
|
+
* // Access by name
|
|
44
|
+
* const sessions = getCache('sessions');
|
|
45
|
+
* const content = getCache('content');
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import type { ControlPanelPlugin, PluginContext } from '../core/types.js';
|
|
52
|
+
|
|
53
|
+
// Dynamic import for ioredis (optional peer dependency)
|
|
54
|
+
type Redis = import('ioredis').default;
|
|
55
|
+
type RedisOptions = import('ioredis').RedisOptions;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Configuration for the cache plugin
|
|
59
|
+
*/
|
|
60
|
+
export interface CachePluginConfig {
|
|
61
|
+
/** Redis connection URL (e.g., redis://localhost:6379) */
|
|
62
|
+
url: string;
|
|
63
|
+
|
|
64
|
+
/** Key prefix for all cache operations (default: '') */
|
|
65
|
+
keyPrefix?: string;
|
|
66
|
+
|
|
67
|
+
/** Default TTL in seconds for set operations (default: 3600 = 1 hour) */
|
|
68
|
+
defaultTtl?: number;
|
|
69
|
+
|
|
70
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
71
|
+
maxRetries?: number;
|
|
72
|
+
|
|
73
|
+
/** Retry delay in milliseconds (default: 1000) */
|
|
74
|
+
retryDelayMs?: number;
|
|
75
|
+
|
|
76
|
+
/** Connection timeout in milliseconds (default: 5000) */
|
|
77
|
+
connectTimeoutMs?: number;
|
|
78
|
+
|
|
79
|
+
/** Command timeout in milliseconds (default: 5000) */
|
|
80
|
+
commandTimeoutMs?: number;
|
|
81
|
+
|
|
82
|
+
/** Register a health check for this cache (default: true) */
|
|
83
|
+
healthCheck?: boolean;
|
|
84
|
+
|
|
85
|
+
/** Name for the health check (default: 'redis') */
|
|
86
|
+
healthCheckName?: string;
|
|
87
|
+
|
|
88
|
+
/** Health check interval in milliseconds (default: 30000) */
|
|
89
|
+
healthCheckInterval?: number;
|
|
90
|
+
|
|
91
|
+
/** Called when connection is ready */
|
|
92
|
+
onConnect?: () => void;
|
|
93
|
+
|
|
94
|
+
/** Called on connection errors */
|
|
95
|
+
onError?: (error: Error) => void;
|
|
96
|
+
|
|
97
|
+
/** Enable lazy connect - don't connect until first command (default: false) */
|
|
98
|
+
lazyConnect?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Cache instance returned by the plugin
|
|
103
|
+
*/
|
|
104
|
+
export interface CacheInstance {
|
|
105
|
+
/**
|
|
106
|
+
* Get a value from cache
|
|
107
|
+
* @param key - Cache key (prefix is applied automatically)
|
|
108
|
+
* @returns Parsed JSON value or null if not found
|
|
109
|
+
*/
|
|
110
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get a raw string value from cache
|
|
114
|
+
* @param key - Cache key (prefix is applied automatically)
|
|
115
|
+
* @returns Raw string value or null if not found
|
|
116
|
+
*/
|
|
117
|
+
getRaw(key: string): Promise<string | null>;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Set a value in cache with TTL
|
|
121
|
+
* @param key - Cache key (prefix is applied automatically)
|
|
122
|
+
* @param value - Value to cache (will be JSON stringified)
|
|
123
|
+
* @param ttlSeconds - Time to live in seconds (uses defaultTtl if not specified)
|
|
124
|
+
*/
|
|
125
|
+
set<T = unknown>(key: string, value: T, ttlSeconds?: number): Promise<void>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Set a raw string value in cache with TTL
|
|
129
|
+
* @param key - Cache key (prefix is applied automatically)
|
|
130
|
+
* @param value - Raw string value to cache
|
|
131
|
+
* @param ttlSeconds - Time to live in seconds (uses defaultTtl if not specified)
|
|
132
|
+
*/
|
|
133
|
+
setRaw(key: string, value: string, ttlSeconds?: number): Promise<void>;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Delete a key from cache
|
|
137
|
+
* @param key - Cache key (prefix is applied automatically)
|
|
138
|
+
* @returns true if key was deleted, false if it didn't exist
|
|
139
|
+
*/
|
|
140
|
+
delete(key: string): Promise<boolean>;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Delete multiple keys matching a pattern
|
|
144
|
+
* @param pattern - Pattern to match (prefix is applied automatically)
|
|
145
|
+
* @returns Number of keys deleted
|
|
146
|
+
*/
|
|
147
|
+
deletePattern(pattern: string): Promise<number>;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if a key exists in cache
|
|
151
|
+
* @param key - Cache key (prefix is applied automatically)
|
|
152
|
+
* @returns true if key exists
|
|
153
|
+
*/
|
|
154
|
+
exists(key: string): Promise<boolean>;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Set expiration time on a key
|
|
158
|
+
* @param key - Cache key (prefix is applied automatically)
|
|
159
|
+
* @param ttlSeconds - Time to live in seconds
|
|
160
|
+
* @returns true if timeout was set, false if key doesn't exist
|
|
161
|
+
*/
|
|
162
|
+
expire(key: string, ttlSeconds: number): Promise<boolean>;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get remaining TTL for a key
|
|
166
|
+
* @param key - Cache key (prefix is applied automatically)
|
|
167
|
+
* @returns TTL in seconds, -1 if no expiry, -2 if key doesn't exist
|
|
168
|
+
*/
|
|
169
|
+
ttl(key: string): Promise<number>;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Increment a numeric value
|
|
173
|
+
* @param key - Cache key (prefix is applied automatically)
|
|
174
|
+
* @param delta - Amount to increment by (default: 1)
|
|
175
|
+
* @returns New value after increment
|
|
176
|
+
*/
|
|
177
|
+
incr(key: string, delta?: number): Promise<number>;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get all keys matching a pattern
|
|
181
|
+
* @param pattern - Pattern to match (prefix is applied automatically)
|
|
182
|
+
* @returns Array of matching keys (without prefix)
|
|
183
|
+
*/
|
|
184
|
+
keys(pattern: string): Promise<string[]>;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Flush all keys with the configured prefix
|
|
188
|
+
* @returns Number of keys deleted
|
|
189
|
+
*/
|
|
190
|
+
flush(): Promise<number>;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get cache statistics
|
|
194
|
+
*/
|
|
195
|
+
getStats(): Promise<{
|
|
196
|
+
connected: boolean;
|
|
197
|
+
keyCount: number;
|
|
198
|
+
usedMemory?: string;
|
|
199
|
+
}>;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get the underlying Redis client (for advanced use cases)
|
|
203
|
+
*/
|
|
204
|
+
getClient(): Redis;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Close the connection
|
|
208
|
+
*/
|
|
209
|
+
close(): Promise<void>;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Global registry of cache instances by name
|
|
213
|
+
const instances = new Map<string, CacheInstance>();
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get a cache instance by name
|
|
217
|
+
*
|
|
218
|
+
* @param name - Instance name (default: 'default')
|
|
219
|
+
* @returns The cache instance
|
|
220
|
+
* @throws Error if the instance is not registered
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```typescript
|
|
224
|
+
* const cache = getCache();
|
|
225
|
+
* const user = await cache.get<User>('user:123');
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
export function getCache(name = 'default'): CacheInstance {
|
|
229
|
+
const instance = instances.get(name);
|
|
230
|
+
if (!instance) {
|
|
231
|
+
throw new Error(`Cache instance "${name}" not found. Did you register the cache plugin?`);
|
|
232
|
+
}
|
|
233
|
+
return instance;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Check if a cache instance is registered
|
|
238
|
+
*
|
|
239
|
+
* @param name - Instance name (default: 'default')
|
|
240
|
+
* @returns true if the instance exists
|
|
241
|
+
*/
|
|
242
|
+
export function hasCache(name = 'default'): boolean {
|
|
243
|
+
return instances.has(name);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Create a cache plugin
|
|
248
|
+
*
|
|
249
|
+
* @param config - Cache configuration
|
|
250
|
+
* @param instanceName - Name for this cache instance (default: 'default')
|
|
251
|
+
* @returns A control panel plugin
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```typescript
|
|
255
|
+
* createCachePlugin({
|
|
256
|
+
* url: process.env.REDIS_URL,
|
|
257
|
+
* keyPrefix: 'myapp:',
|
|
258
|
+
* defaultTtl: 3600,
|
|
259
|
+
* healthCheck: true,
|
|
260
|
+
* });
|
|
261
|
+
* ```
|
|
262
|
+
*/
|
|
263
|
+
export function createCachePlugin(
|
|
264
|
+
config: CachePluginConfig,
|
|
265
|
+
instanceName = 'default'
|
|
266
|
+
): ControlPanelPlugin {
|
|
267
|
+
let client: Redis | null = null;
|
|
268
|
+
const prefix = config.keyPrefix ?? '';
|
|
269
|
+
const defaultTtl = config.defaultTtl ?? 3600;
|
|
270
|
+
|
|
271
|
+
const prefixKey = (key: string): string => `${prefix}${key}`;
|
|
272
|
+
const unprefixKey = (key: string): string =>
|
|
273
|
+
prefix && key.startsWith(prefix) ? key.slice(prefix.length) : key;
|
|
274
|
+
|
|
275
|
+
const createInstance = async (): Promise<CacheInstance> => {
|
|
276
|
+
// Dynamic import of ioredis
|
|
277
|
+
const { default: Redis } = await import('ioredis');
|
|
278
|
+
|
|
279
|
+
const options: RedisOptions = {
|
|
280
|
+
maxRetriesPerRequest: config.maxRetries ?? 3,
|
|
281
|
+
retryStrategy: (times: number) => {
|
|
282
|
+
const maxRetries = config.maxRetries ?? 3;
|
|
283
|
+
if (times > maxRetries) {
|
|
284
|
+
console.error(`[cache:${instanceName}] Connection failed after ${maxRetries} retries`);
|
|
285
|
+
return null; // Stop retrying
|
|
286
|
+
}
|
|
287
|
+
return config.retryDelayMs ?? 1000;
|
|
288
|
+
},
|
|
289
|
+
connectTimeout: config.connectTimeoutMs ?? 5000,
|
|
290
|
+
commandTimeout: config.commandTimeoutMs ?? 5000,
|
|
291
|
+
lazyConnect: config.lazyConnect ?? false,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
client = new Redis(config.url, options);
|
|
295
|
+
|
|
296
|
+
// Handle events
|
|
297
|
+
client.on('error', (err) => {
|
|
298
|
+
if (config.onError) {
|
|
299
|
+
config.onError(err);
|
|
300
|
+
} else {
|
|
301
|
+
console.error(`[cache:${instanceName}] Error:`, err.message);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
client.on('connect', () => {
|
|
306
|
+
if (config.onConnect) {
|
|
307
|
+
config.onConnect();
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const instance: CacheInstance = {
|
|
312
|
+
async get<T = unknown>(key: string): Promise<T | null> {
|
|
313
|
+
if (!client) throw new Error('Cache client not initialized');
|
|
314
|
+
const value = await client.get(prefixKey(key));
|
|
315
|
+
if (value === null) return null;
|
|
316
|
+
try {
|
|
317
|
+
return JSON.parse(value) as T;
|
|
318
|
+
} catch {
|
|
319
|
+
return value as unknown as T;
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
async getRaw(key: string): Promise<string | null> {
|
|
324
|
+
if (!client) throw new Error('Cache client not initialized');
|
|
325
|
+
return client.get(prefixKey(key));
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
async set<T = unknown>(key: string, value: T, ttlSeconds?: number): Promise<void> {
|
|
329
|
+
if (!client) throw new Error('Cache client not initialized');
|
|
330
|
+
const ttl = ttlSeconds ?? defaultTtl;
|
|
331
|
+
const serialized = typeof value === 'string' ? value : JSON.stringify(value);
|
|
332
|
+
await client.setex(prefixKey(key), ttl, serialized);
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
async setRaw(key: string, value: string, ttlSeconds?: number): Promise<void> {
|
|
336
|
+
if (!client) throw new Error('Cache client not initialized');
|
|
337
|
+
const ttl = ttlSeconds ?? defaultTtl;
|
|
338
|
+
await client.setex(prefixKey(key), ttl, value);
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
async delete(key: string): Promise<boolean> {
|
|
342
|
+
if (!client) throw new Error('Cache client not initialized');
|
|
343
|
+
const count = await client.del(prefixKey(key));
|
|
344
|
+
return count > 0;
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
async deletePattern(pattern: string): Promise<number> {
|
|
348
|
+
if (!client) throw new Error('Cache client not initialized');
|
|
349
|
+
const keys = await client.keys(prefixKey(pattern));
|
|
350
|
+
if (keys.length === 0) return 0;
|
|
351
|
+
return client.del(...keys);
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
async exists(key: string): Promise<boolean> {
|
|
355
|
+
if (!client) throw new Error('Cache client not initialized');
|
|
356
|
+
const count = await client.exists(prefixKey(key));
|
|
357
|
+
return count > 0;
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
async expire(key: string, ttlSeconds: number): Promise<boolean> {
|
|
361
|
+
if (!client) throw new Error('Cache client not initialized');
|
|
362
|
+
const result = await client.expire(prefixKey(key), ttlSeconds);
|
|
363
|
+
return result === 1;
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
async ttl(key: string): Promise<number> {
|
|
367
|
+
if (!client) throw new Error('Cache client not initialized');
|
|
368
|
+
return client.ttl(prefixKey(key));
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
async incr(key: string, delta = 1): Promise<number> {
|
|
372
|
+
if (!client) throw new Error('Cache client not initialized');
|
|
373
|
+
if (delta === 1) {
|
|
374
|
+
return client.incr(prefixKey(key));
|
|
375
|
+
}
|
|
376
|
+
return client.incrby(prefixKey(key), delta);
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
async keys(pattern: string): Promise<string[]> {
|
|
380
|
+
if (!client) throw new Error('Cache client not initialized');
|
|
381
|
+
const keys = await client.keys(prefixKey(pattern));
|
|
382
|
+
return keys.map(unprefixKey);
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
async flush(): Promise<number> {
|
|
386
|
+
if (!client) throw new Error('Cache client not initialized');
|
|
387
|
+
if (!prefix) {
|
|
388
|
+
// Without prefix, this would flush the entire database - dangerous!
|
|
389
|
+
throw new Error('Cannot flush without a keyPrefix configured');
|
|
390
|
+
}
|
|
391
|
+
const keys = await client.keys(`${prefix}*`);
|
|
392
|
+
if (keys.length === 0) return 0;
|
|
393
|
+
return client.del(...keys);
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
async getStats(): Promise<{ connected: boolean; keyCount: number; usedMemory?: string }> {
|
|
397
|
+
if (!client) {
|
|
398
|
+
return { connected: false, keyCount: 0 };
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
const info = await client.info('memory');
|
|
402
|
+
const memoryMatch = info.match(/used_memory_human:(\S+)/);
|
|
403
|
+
const usedMemory = memoryMatch ? memoryMatch[1] : undefined;
|
|
404
|
+
|
|
405
|
+
const keys = prefix
|
|
406
|
+
? await client.keys(`${prefix}*`)
|
|
407
|
+
: await client.dbsize();
|
|
408
|
+
const keyCount = typeof keys === 'number' ? keys : keys.length;
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
connected: client.status === 'ready',
|
|
412
|
+
keyCount,
|
|
413
|
+
usedMemory,
|
|
414
|
+
};
|
|
415
|
+
} catch {
|
|
416
|
+
return {
|
|
417
|
+
connected: client.status === 'ready',
|
|
418
|
+
keyCount: 0,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
getClient(): Redis {
|
|
424
|
+
if (!client) throw new Error('Cache client not initialized');
|
|
425
|
+
return client;
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
async close(): Promise<void> {
|
|
429
|
+
if (client) {
|
|
430
|
+
await client.quit();
|
|
431
|
+
client = null;
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
return instance;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
name: `cache:${instanceName}`,
|
|
441
|
+
order: 5, // Initialize early, before other plugins that may need cache
|
|
442
|
+
|
|
443
|
+
async onInit(context: PluginContext): Promise<void> {
|
|
444
|
+
const { registerHealthCheck, logger } = context;
|
|
445
|
+
|
|
446
|
+
// Create and register the instance
|
|
447
|
+
const instance = await createInstance();
|
|
448
|
+
instances.set(instanceName, instance);
|
|
449
|
+
|
|
450
|
+
// Test connection
|
|
451
|
+
try {
|
|
452
|
+
// Ping to verify connection
|
|
453
|
+
await instance.getClient().ping();
|
|
454
|
+
logger.debug(`Cache "${instanceName}" connected`);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
logger.error(`Cache "${instanceName}" connection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
457
|
+
throw err;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Register health check if enabled
|
|
461
|
+
if (config.healthCheck !== false) {
|
|
462
|
+
registerHealthCheck({
|
|
463
|
+
name: config.healthCheckName ?? 'redis',
|
|
464
|
+
type: 'custom',
|
|
465
|
+
interval: config.healthCheckInterval ?? 30000,
|
|
466
|
+
timeout: 5000,
|
|
467
|
+
check: async () => {
|
|
468
|
+
const start = Date.now();
|
|
469
|
+
try {
|
|
470
|
+
await instance.getClient().ping();
|
|
471
|
+
const stats = await instance.getStats();
|
|
472
|
+
return {
|
|
473
|
+
healthy: true,
|
|
474
|
+
latency: Date.now() - start,
|
|
475
|
+
details: {
|
|
476
|
+
connected: stats.connected,
|
|
477
|
+
keyCount: stats.keyCount,
|
|
478
|
+
usedMemory: stats.usedMemory,
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
} catch (err) {
|
|
482
|
+
return {
|
|
483
|
+
healthy: false,
|
|
484
|
+
latency: Date.now() - start,
|
|
485
|
+
details: {
|
|
486
|
+
error: err instanceof Error ? err.message : String(err),
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
async onShutdown(): Promise<void> {
|
|
496
|
+
const instance = instances.get(instanceName);
|
|
497
|
+
if (instance) {
|
|
498
|
+
await instance.close();
|
|
499
|
+
instances.delete(instanceName);
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
}
|
|
@@ -101,7 +101,7 @@ export function createConfigPlugin(config: ConfigPluginConfig): ControlPanelPlug
|
|
|
101
101
|
],
|
|
102
102
|
|
|
103
103
|
async onInit(context: PluginContext): Promise<void> {
|
|
104
|
-
context.logger.
|
|
104
|
+
context.logger.debug(`Config plugin initialized with ${config.show.length} vars`);
|
|
105
105
|
},
|
|
106
106
|
};
|
|
107
107
|
}
|
|
@@ -142,7 +142,7 @@ export function createDiagnosticsPlugin(config: DiagnosticsPluginConfig = {}): C
|
|
|
142
142
|
],
|
|
143
143
|
|
|
144
144
|
async onInit(context: PluginContext): Promise<void> {
|
|
145
|
-
context.logger.
|
|
145
|
+
context.logger.debug('Diagnostics plugin initialized');
|
|
146
146
|
},
|
|
147
147
|
};
|
|
148
148
|
}
|
|
@@ -29,7 +29,7 @@ export function createHealthPlugin(config: HealthPluginConfig): ControlPanelPlug
|
|
|
29
29
|
registerHealthCheck(check);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
logger.
|
|
32
|
+
logger.debug(`Registered ${config.checks.length} health checks`);
|
|
33
33
|
},
|
|
34
34
|
};
|
|
35
35
|
}
|
package/src/plugins/index.ts
CHANGED
|
@@ -18,3 +18,13 @@ export type { DiagnosticsPluginConfig } from './diagnostics-plugin.js';
|
|
|
18
18
|
|
|
19
19
|
export { createFrontendAppPlugin } from './frontend-app-plugin.js';
|
|
20
20
|
export type { FrontendAppPluginConfig } from './frontend-app-plugin.js';
|
|
21
|
+
|
|
22
|
+
export { createPostgresPlugin, getPostgres, hasPostgres } from './postgres-plugin.js';
|
|
23
|
+
export type { PostgresPluginConfig, PostgresInstance, TransactionCallback } from './postgres-plugin.js';
|
|
24
|
+
|
|
25
|
+
// Backward compatibility aliases (deprecated)
|
|
26
|
+
export { createPostgresPlugin as createDatabasePlugin, getPostgres as getDatabase, hasPostgres as hasDatabase } from './postgres-plugin.js';
|
|
27
|
+
export type { PostgresPluginConfig as DatabasePluginConfig, PostgresInstance as DatabaseInstance } from './postgres-plugin.js';
|
|
28
|
+
|
|
29
|
+
export { createCachePlugin, getCache, hasCache } from './cache-plugin.js';
|
|
30
|
+
export type { CachePluginConfig, CacheInstance } from './cache-plugin.js';
|
|
@@ -156,9 +156,7 @@ export function createLogsPlugin(config: LogsPluginConfig = {}): ControlPanelPlu
|
|
|
156
156
|
|
|
157
157
|
async onInit(context: PluginContext): Promise<void> {
|
|
158
158
|
const sources = getSources();
|
|
159
|
-
context.logger.
|
|
160
|
-
sources: sources.map((s) => ({ name: s.name, type: s.type, path: s.path })),
|
|
161
|
-
});
|
|
159
|
+
context.logger.debug(`Logs plugin initialized with ${sources.length} sources`);
|
|
162
160
|
},
|
|
163
161
|
};
|
|
164
162
|
}
|