request-scope-api 1.0.21 → 1.0.23
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/dist/cjs/adapter-singleton.js +144 -0
- package/dist/cjs/adapter-singleton.js.map +1 -0
- package/dist/cjs/adapters/mongo.adapter.js +10 -0
- package/dist/cjs/adapters/mongo.adapter.js.map +1 -1
- package/dist/cjs/adapters/mysql.adapter.js +9 -0
- package/dist/cjs/adapters/mysql.adapter.js.map +1 -1
- package/dist/cjs/adapters/pg.adapter.js +9 -0
- package/dist/cjs/adapters/pg.adapter.js.map +1 -1
- package/dist/cjs/index.js +22 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/middleware.js +23 -52
- package/dist/cjs/middleware.js.map +1 -1
- package/dist/cjs/retention.js +4 -0
- package/dist/cjs/retention.js.map +1 -1
- package/dist/cjs/shutdown-manager.js +120 -0
- package/dist/cjs/shutdown-manager.js.map +1 -0
- package/dist/esm/adapter-singleton.js +140 -0
- package/dist/esm/adapters/mongo.adapter.js +10 -0
- package/dist/esm/adapters/mysql.adapter.js +9 -0
- package/dist/esm/adapters/pg.adapter.js +9 -0
- package/dist/esm/index.js +21 -0
- package/dist/esm/middleware.js +23 -52
- package/dist/esm/retention.js +4 -0
- package/dist/esm/shutdown-manager.js +116 -0
- package/dist/types/adapter-singleton.d.ts +52 -0
- package/dist/types/adapters/mongo.adapter.d.ts +1 -0
- package/dist/types/adapters/mysql.adapter.d.ts +1 -0
- package/dist/types/adapters/pg.adapter.d.ts +1 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/shutdown-manager.d.ts +49 -0
- package/dist/types/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ShutdownManager — Manages graceful shutdown of all RequestScope resources.
|
|
4
|
+
*
|
|
5
|
+
* This centralizes shutdown logic to prevent multiple shutdown executions,
|
|
6
|
+
* add comprehensive logging, and expose a shutdown() method that the host
|
|
7
|
+
* application can call instead of relying on global signal handlers.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.shutdownManager = void 0;
|
|
11
|
+
const adapter_singleton_js_1 = require("./adapter-singleton.js");
|
|
12
|
+
class ShutdownManager {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.isShuttingDown = false;
|
|
15
|
+
this.resources = {};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Registers resources that need to be cleaned up during shutdown.
|
|
19
|
+
*/
|
|
20
|
+
registerResources(resources) {
|
|
21
|
+
this.resources = { ...this.resources, ...resources };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Performs graceful shutdown of all registered resources.
|
|
25
|
+
*
|
|
26
|
+
* Steps:
|
|
27
|
+
* 1. Prevents multiple shutdown executions
|
|
28
|
+
* 2. Stops retention scheduler
|
|
29
|
+
* 3. Drains queue with timeout
|
|
30
|
+
* 4. Destroys queue
|
|
31
|
+
* 5. Closes all database connections
|
|
32
|
+
* 6. Logs remaining active handles for debugging
|
|
33
|
+
*/
|
|
34
|
+
async shutdown(options = {}) {
|
|
35
|
+
const { drainTimeoutMs = 5000, logHandles = false } = options;
|
|
36
|
+
// Prevent multiple shutdown executions
|
|
37
|
+
if (this.isShuttingDown) {
|
|
38
|
+
process.stderr.write('[RequestScope] Shutdown already in progress, skipping duplicate call\n');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
this.isShuttingDown = true;
|
|
42
|
+
process.stderr.write('[RequestScope] Starting graceful shutdown...\n');
|
|
43
|
+
const startTime = Date.now();
|
|
44
|
+
try {
|
|
45
|
+
// 1. Stop retention scheduler
|
|
46
|
+
if (this.resources.retentionScheduler) {
|
|
47
|
+
process.stderr.write('[RequestScope] Stopping retention scheduler...\n');
|
|
48
|
+
this.resources.retentionScheduler.stop();
|
|
49
|
+
}
|
|
50
|
+
// 2. Drain queue with timeout
|
|
51
|
+
if (this.resources.queue) {
|
|
52
|
+
process.stderr.write(`[RequestScope] Draining queue (timeout: ${drainTimeoutMs}ms)...\n`);
|
|
53
|
+
try {
|
|
54
|
+
await Promise.race([
|
|
55
|
+
this.resources.queue.drain(drainTimeoutMs),
|
|
56
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Queue drain timeout')), drainTimeoutMs))
|
|
57
|
+
]);
|
|
58
|
+
process.stderr.write('[RequestScope] Queue drained successfully\n');
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
62
|
+
process.stderr.write(`[RequestScope] Queue drain warning: ${message}\n`);
|
|
63
|
+
}
|
|
64
|
+
// 3. Destroy queue (clears timer)
|
|
65
|
+
process.stderr.write('[RequestScope] Destroying queue...\n');
|
|
66
|
+
this.resources.queue.destroy();
|
|
67
|
+
}
|
|
68
|
+
// 4. Close all database connections
|
|
69
|
+
process.stderr.write('[RequestScope] Closing database connections...\n');
|
|
70
|
+
await adapter_singleton_js_1.adapterSingleton.closeAll();
|
|
71
|
+
process.stderr.write('[RequestScope] Database connections closed\n');
|
|
72
|
+
const duration = Date.now() - startTime;
|
|
73
|
+
process.stderr.write(`[RequestScope] Graceful shutdown completed in ${duration}ms\n`);
|
|
74
|
+
// 5. Log remaining active handles for debugging
|
|
75
|
+
if (logHandles) {
|
|
76
|
+
this.logActiveHandles();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
81
|
+
process.stderr.write(`[RequestScope] Shutdown error: ${message}\n`);
|
|
82
|
+
// Log active handles even on error
|
|
83
|
+
if (logHandles) {
|
|
84
|
+
this.logActiveHandles();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Logs active handles to help identify what's preventing process exit.
|
|
90
|
+
*/
|
|
91
|
+
logActiveHandles() {
|
|
92
|
+
try {
|
|
93
|
+
// @ts-ignore - _getActiveHandles is not in TypeScript definitions
|
|
94
|
+
const handles = process._getActiveHandles();
|
|
95
|
+
process.stderr.write(`[RequestScope] Active handles (${handles.length}):\n`);
|
|
96
|
+
for (let i = 0; i < Math.min(handles.length, 20); i++) {
|
|
97
|
+
const handle = handles[i];
|
|
98
|
+
const type = handle.constructor.name;
|
|
99
|
+
// @ts-ignore
|
|
100
|
+
const details = handle._handle?.type || '';
|
|
101
|
+
process.stderr.write(` [${i}] ${type}${details ? ` (${details})` : ''}\n`);
|
|
102
|
+
}
|
|
103
|
+
if (handles.length > 20) {
|
|
104
|
+
process.stderr.write(` ... and ${handles.length - 20} more\n`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
process.stderr.write('[RequestScope] Could not log active handles\n');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Returns whether shutdown is currently in progress.
|
|
113
|
+
*/
|
|
114
|
+
isShutdownInProgress() {
|
|
115
|
+
return this.isShuttingDown;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Export singleton instance
|
|
119
|
+
exports.shutdownManager = new ShutdownManager();
|
|
120
|
+
//# sourceMappingURL=shutdown-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shutdown-manager.js","sourceRoot":"","sources":["../../src/shutdown-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdapterSingleton — Manages singleton database adapter instances.
|
|
3
|
+
*
|
|
4
|
+
* Ensures that only one database connection/pool exists per unique configuration
|
|
5
|
+
* across the entire application lifecycle. This prevents:
|
|
6
|
+
* - Multiple connection pools when middleware is called multiple times
|
|
7
|
+
* - Duplicate connections when setup function is imported multiple times
|
|
8
|
+
* - Connection leaks on server restarts in dev mode (nodemon)
|
|
9
|
+
*
|
|
10
|
+
* The singleton is keyed by a hash of the storage configuration, so different
|
|
11
|
+
* configurations will have separate connections (as expected).
|
|
12
|
+
*/
|
|
13
|
+
import { MongoAdapter } from './adapters/mongo.adapter.js';
|
|
14
|
+
import { MySQLAdapter } from './adapters/mysql.adapter.js';
|
|
15
|
+
import { PgAdapter } from './adapters/pg.adapter.js';
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Configuration hash helper
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/**
|
|
20
|
+
* Creates a stable hash string from storage configuration.
|
|
21
|
+
* This is used as the key for singleton instances.
|
|
22
|
+
*/
|
|
23
|
+
function hashStorageConfig(config) {
|
|
24
|
+
const parts = [config.type];
|
|
25
|
+
if (config.type === 'mongodb') {
|
|
26
|
+
parts.push(config.uri || '');
|
|
27
|
+
}
|
|
28
|
+
else if (config.type === 'mysql' || config.type === 'postgresql') {
|
|
29
|
+
parts.push(config.host || 'localhost', String(config.port || (config.type === 'mysql' ? 3306 : 5432)), config.database || '', config.username || '');
|
|
30
|
+
}
|
|
31
|
+
return parts.join(':');
|
|
32
|
+
}
|
|
33
|
+
class AdapterSingletonManager {
|
|
34
|
+
constructor() {
|
|
35
|
+
this.instances = new Map();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Gets or creates a singleton adapter instance for the given configuration.
|
|
39
|
+
*
|
|
40
|
+
* @param config - Storage configuration
|
|
41
|
+
* @returns StorageAdapter instance (shared if already exists)
|
|
42
|
+
*/
|
|
43
|
+
getOrCreate(config) {
|
|
44
|
+
const hash = hashStorageConfig(config);
|
|
45
|
+
const existing = this.instances.get(hash);
|
|
46
|
+
if (existing) {
|
|
47
|
+
// Increment reference count
|
|
48
|
+
existing.refCount++;
|
|
49
|
+
return existing.adapter;
|
|
50
|
+
}
|
|
51
|
+
// Create new adapter instance
|
|
52
|
+
let adapter;
|
|
53
|
+
switch (config.type) {
|
|
54
|
+
case 'mongodb':
|
|
55
|
+
adapter = new MongoAdapter(config);
|
|
56
|
+
break;
|
|
57
|
+
case 'mysql':
|
|
58
|
+
adapter = new MySQLAdapter(config);
|
|
59
|
+
break;
|
|
60
|
+
case 'postgresql':
|
|
61
|
+
adapter = new PgAdapter(config);
|
|
62
|
+
break;
|
|
63
|
+
default:
|
|
64
|
+
throw new Error(`Unsupported storage type: ${config.type}`);
|
|
65
|
+
}
|
|
66
|
+
// Store in singleton map
|
|
67
|
+
this.instances.set(hash, {
|
|
68
|
+
adapter,
|
|
69
|
+
refCount: 1,
|
|
70
|
+
initialized: false,
|
|
71
|
+
});
|
|
72
|
+
return adapter;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Marks an adapter as initialized.
|
|
76
|
+
* This is called after successful adapter.initialize().
|
|
77
|
+
*/
|
|
78
|
+
markInitialized(adapter, config) {
|
|
79
|
+
const hash = hashStorageConfig(config);
|
|
80
|
+
const entry = this.instances.get(hash);
|
|
81
|
+
if (entry && entry.adapter === adapter) {
|
|
82
|
+
entry.initialized = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Decrements reference count for an adapter.
|
|
87
|
+
* When reference count reaches zero, the adapter is cleaned up.
|
|
88
|
+
*
|
|
89
|
+
* @param adapter - StorageAdapter instance to release
|
|
90
|
+
* @param config - Storage configuration used to create the adapter
|
|
91
|
+
*/
|
|
92
|
+
release(adapter, config) {
|
|
93
|
+
const hash = hashStorageConfig(config);
|
|
94
|
+
const entry = this.instances.get(hash);
|
|
95
|
+
if (entry && entry.adapter === adapter) {
|
|
96
|
+
entry.refCount--;
|
|
97
|
+
// Clean up if no more references
|
|
98
|
+
if (entry.refCount <= 0) {
|
|
99
|
+
this.instances.delete(hash);
|
|
100
|
+
// Close the connection to prevent connection leaks
|
|
101
|
+
void adapter.close().catch((err) => {
|
|
102
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
103
|
+
process.stderr.write(`[RequestScope] Error closing adapter: ${message}\n`);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Gets the current number of singleton instances (for debugging).
|
|
110
|
+
*/
|
|
111
|
+
getInstanceCount() {
|
|
112
|
+
return this.instances.size;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Clears all singleton instances.
|
|
116
|
+
* This should only be used in tests or on application shutdown.
|
|
117
|
+
*/
|
|
118
|
+
clearAll() {
|
|
119
|
+
this.instances.clear();
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Closes all adapter connections and clears the singleton instances.
|
|
123
|
+
* This should be called during application graceful shutdown.
|
|
124
|
+
*/
|
|
125
|
+
async closeAll() {
|
|
126
|
+
const closePromises = [];
|
|
127
|
+
for (const [hash, entry] of this.instances.entries()) {
|
|
128
|
+
closePromises.push(entry.adapter.close().catch((err) => {
|
|
129
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
130
|
+
process.stderr.write(`[RequestScope] Error closing adapter [${hash}]: ${message}\n`);
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
await Promise.all(closePromises);
|
|
134
|
+
this.instances.clear();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Export singleton instance
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
export const adapterSingleton = new AdapterSingletonManager();
|
|
@@ -180,4 +180,14 @@ export class MongoAdapter {
|
|
|
180
180
|
});
|
|
181
181
|
return result.deletedCount;
|
|
182
182
|
}
|
|
183
|
+
// -------------------------------------------------------------------------
|
|
184
|
+
// close()
|
|
185
|
+
// -------------------------------------------------------------------------
|
|
186
|
+
async close() {
|
|
187
|
+
if (this.client) {
|
|
188
|
+
await this.client.close();
|
|
189
|
+
this.client = null;
|
|
190
|
+
this.collection = null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
183
193
|
}
|
|
@@ -224,6 +224,15 @@ export class MySQLAdapter {
|
|
|
224
224
|
return result.affectedRows;
|
|
225
225
|
}
|
|
226
226
|
// -------------------------------------------------------------------------
|
|
227
|
+
// close()
|
|
228
|
+
// -------------------------------------------------------------------------
|
|
229
|
+
async close() {
|
|
230
|
+
if (this.pool) {
|
|
231
|
+
await this.pool.end();
|
|
232
|
+
this.pool = null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// -------------------------------------------------------------------------
|
|
227
236
|
// Private helpers
|
|
228
237
|
// -------------------------------------------------------------------------
|
|
229
238
|
getPool() {
|
|
@@ -320,6 +320,15 @@ export class PgAdapter {
|
|
|
320
320
|
return result.rowCount ?? 0;
|
|
321
321
|
}
|
|
322
322
|
// -------------------------------------------------------------------------
|
|
323
|
+
// close()
|
|
324
|
+
// -------------------------------------------------------------------------
|
|
325
|
+
async close() {
|
|
326
|
+
if (this.pool) {
|
|
327
|
+
await this.pool.end();
|
|
328
|
+
this.pool = null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// -------------------------------------------------------------------------
|
|
323
332
|
// Private helpers
|
|
324
333
|
// -------------------------------------------------------------------------
|
|
325
334
|
getPool() {
|
package/dist/esm/index.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import express from 'express';
|
|
12
12
|
import { requestscope as requestscopeMiddleware, errorHandler, setup } from './middleware.js';
|
|
13
13
|
import { createDashboardRouter } from './dashboard/router.js';
|
|
14
|
+
import { shutdownManager } from './shutdown-manager.js';
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Main middleware factory
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
@@ -55,6 +56,26 @@ export { setup };
|
|
|
55
56
|
// ---------------------------------------------------------------------------
|
|
56
57
|
export { errorHandler };
|
|
57
58
|
// ---------------------------------------------------------------------------
|
|
59
|
+
// Shutdown function
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
/**
|
|
62
|
+
* Gracefully shuts down all RequestScope resources.
|
|
63
|
+
*
|
|
64
|
+
* This should be called by the host application during shutdown (e.g., on SIGTERM/SIGINT).
|
|
65
|
+
* It will:
|
|
66
|
+
* - Stop the retention scheduler
|
|
67
|
+
* - Drain the queue with a timeout
|
|
68
|
+
* - Destroy the queue (clear timers)
|
|
69
|
+
* - Close all database connections
|
|
70
|
+
*
|
|
71
|
+
* @param options - Optional configuration for shutdown
|
|
72
|
+
* @param options.drainTimeoutMs - Timeout for queue drain in milliseconds (default: 5000)
|
|
73
|
+
* @param options.logHandles - Whether to log active handles after shutdown (default: false)
|
|
74
|
+
*/
|
|
75
|
+
export async function shutdown(options) {
|
|
76
|
+
await shutdownManager.shutdown(options);
|
|
77
|
+
}
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
58
79
|
// Named exports for convenience
|
|
59
80
|
// ---------------------------------------------------------------------------
|
|
60
81
|
export { requestscope as requestscopeMiddleware } from './middleware.js';
|
package/dist/esm/middleware.js
CHANGED
|
@@ -11,13 +11,12 @@
|
|
|
11
11
|
* Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 2.2, 4.1, 4.2, 4.3, 5.1
|
|
12
12
|
*/
|
|
13
13
|
import { validateConfig, applyDefaults, buildSensitiveFieldSet } from './config.js';
|
|
14
|
-
import { MongoAdapter } from './adapters/mongo.adapter.js';
|
|
15
|
-
import { MySQLAdapter } from './adapters/mysql.adapter.js';
|
|
16
|
-
import { PgAdapter } from './adapters/pg.adapter.js';
|
|
17
14
|
import { AsyncQueue } from './queue.js';
|
|
18
15
|
import { RetentionScheduler } from './retention.js';
|
|
19
16
|
import { bufferRequestBody, wrapResponse, normalizeRequestUrl, ERROR_DATA_SYMBOL, } from './capture.js';
|
|
20
17
|
import { createDashboardRouter } from './dashboard/router.js';
|
|
18
|
+
import { adapterSingleton } from './adapter-singleton.js';
|
|
19
|
+
import { shutdownManager } from './shutdown-manager.js';
|
|
21
20
|
const ALWAYS_IGNORED_PATHS = ['/favicon.ico', '/requestscope/api', '/requestscope/assets'];
|
|
22
21
|
// ---------------------------------------------------------------------------
|
|
23
22
|
// Factory function
|
|
@@ -56,25 +55,15 @@ export function requestscope(config, adapter) {
|
|
|
56
55
|
validateConfig(config);
|
|
57
56
|
applyDefaults(config);
|
|
58
57
|
const sensitiveFields = buildSensitiveFieldSet(config);
|
|
59
|
-
// 2.
|
|
58
|
+
// 2. Get or create singleton adapter instance
|
|
60
59
|
if (!adapter) {
|
|
61
|
-
|
|
62
|
-
case 'mongodb':
|
|
63
|
-
adapter = new MongoAdapter(config.storage);
|
|
64
|
-
break;
|
|
65
|
-
case 'mysql':
|
|
66
|
-
adapter = new MySQLAdapter(config.storage);
|
|
67
|
-
break;
|
|
68
|
-
case 'postgresql':
|
|
69
|
-
adapter = new PgAdapter(config.storage);
|
|
70
|
-
break;
|
|
71
|
-
default:
|
|
72
|
-
// This should never happen due to validateConfig, but TypeScript needs it
|
|
73
|
-
throw new Error(`Unsupported storage type: ${config.storage.type}`);
|
|
74
|
-
}
|
|
60
|
+
adapter = adapterSingleton.getOrCreate(config.storage);
|
|
75
61
|
}
|
|
76
62
|
// 3. Initialize adapter asynchronously (log errors, don't throw)
|
|
77
|
-
|
|
63
|
+
// Only initialize if not already initialized
|
|
64
|
+
void adapter.initialize().then(() => {
|
|
65
|
+
adapterSingleton.markInitialized(adapter, config.storage);
|
|
66
|
+
}).catch((err) => {
|
|
78
67
|
const message = err instanceof Error ? err.message : String(err);
|
|
79
68
|
process.stderr.write(`[RequestScope] Failed to initialize storage adapter: ${message}\n`);
|
|
80
69
|
});
|
|
@@ -83,23 +72,16 @@ export function requestscope(config, adapter) {
|
|
|
83
72
|
const retentionDays = config.retentionDays ?? 30;
|
|
84
73
|
const retentionScheduler = new RetentionScheduler(adapter, retentionDays);
|
|
85
74
|
retentionScheduler.start();
|
|
86
|
-
// 5.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
process.on('SIGTERM', () => {
|
|
97
|
-
void shutdown();
|
|
98
|
-
});
|
|
99
|
-
process.on('SIGINT', () => {
|
|
100
|
-
void shutdown();
|
|
101
|
-
});
|
|
102
|
-
}
|
|
75
|
+
// 5. Register resources with shutdown manager
|
|
76
|
+
shutdownManager.registerResources({
|
|
77
|
+
queue: {
|
|
78
|
+
drain: (timeoutMs) => queue.drain(timeoutMs),
|
|
79
|
+
destroy: () => queue.destroy(),
|
|
80
|
+
},
|
|
81
|
+
retentionScheduler: {
|
|
82
|
+
stop: () => retentionScheduler.stop(),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
103
85
|
// 6. Return the request handler middleware
|
|
104
86
|
return async (req, res, next) => {
|
|
105
87
|
// Check ignore list
|
|
@@ -153,23 +135,12 @@ export function setup(app, config) {
|
|
|
153
135
|
// Validate and apply defaults
|
|
154
136
|
validateConfig(config);
|
|
155
137
|
applyDefaults(config);
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
switch (config.storage.type) {
|
|
159
|
-
case 'mongodb':
|
|
160
|
-
adapter = new MongoAdapter(config.storage);
|
|
161
|
-
break;
|
|
162
|
-
case 'mysql':
|
|
163
|
-
adapter = new MySQLAdapter(config.storage);
|
|
164
|
-
break;
|
|
165
|
-
case 'postgresql':
|
|
166
|
-
adapter = new PgAdapter(config.storage);
|
|
167
|
-
break;
|
|
168
|
-
default:
|
|
169
|
-
throw new Error(`Unsupported storage type: ${config.storage.type}`);
|
|
170
|
-
}
|
|
138
|
+
// Get or create singleton adapter instance
|
|
139
|
+
const adapter = adapterSingleton.getOrCreate(config.storage);
|
|
171
140
|
// Initialize adapter asynchronously
|
|
172
|
-
void adapter.initialize().
|
|
141
|
+
void adapter.initialize().then(() => {
|
|
142
|
+
adapterSingleton.markInitialized(adapter, config.storage);
|
|
143
|
+
}).catch((err) => {
|
|
173
144
|
const message = err instanceof Error ? err.message : String(err);
|
|
174
145
|
process.stderr.write(`[RequestScope] Failed to initialize storage adapter: ${message}\n`);
|
|
175
146
|
});
|
package/dist/esm/retention.js
CHANGED
|
@@ -27,6 +27,10 @@ export class RetentionScheduler {
|
|
|
27
27
|
this.intervalHandle = setInterval(() => {
|
|
28
28
|
void this.runOnce();
|
|
29
29
|
}, TWENTY_FOUR_HOURS_MS);
|
|
30
|
+
// Allow the Node.js process to exit even if this timer is still active.
|
|
31
|
+
if (this.intervalHandle.unref) {
|
|
32
|
+
this.intervalHandle.unref();
|
|
33
|
+
}
|
|
30
34
|
}
|
|
31
35
|
/**
|
|
32
36
|
* Stops the scheduled retention job. Inflight `runOnce()` calls are allowed
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShutdownManager — Manages graceful shutdown of all RequestScope resources.
|
|
3
|
+
*
|
|
4
|
+
* This centralizes shutdown logic to prevent multiple shutdown executions,
|
|
5
|
+
* add comprehensive logging, and expose a shutdown() method that the host
|
|
6
|
+
* application can call instead of relying on global signal handlers.
|
|
7
|
+
*/
|
|
8
|
+
import { adapterSingleton } from './adapter-singleton.js';
|
|
9
|
+
class ShutdownManager {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.isShuttingDown = false;
|
|
12
|
+
this.resources = {};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Registers resources that need to be cleaned up during shutdown.
|
|
16
|
+
*/
|
|
17
|
+
registerResources(resources) {
|
|
18
|
+
this.resources = { ...this.resources, ...resources };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Performs graceful shutdown of all registered resources.
|
|
22
|
+
*
|
|
23
|
+
* Steps:
|
|
24
|
+
* 1. Prevents multiple shutdown executions
|
|
25
|
+
* 2. Stops retention scheduler
|
|
26
|
+
* 3. Drains queue with timeout
|
|
27
|
+
* 4. Destroys queue
|
|
28
|
+
* 5. Closes all database connections
|
|
29
|
+
* 6. Logs remaining active handles for debugging
|
|
30
|
+
*/
|
|
31
|
+
async shutdown(options = {}) {
|
|
32
|
+
const { drainTimeoutMs = 5000, logHandles = false } = options;
|
|
33
|
+
// Prevent multiple shutdown executions
|
|
34
|
+
if (this.isShuttingDown) {
|
|
35
|
+
process.stderr.write('[RequestScope] Shutdown already in progress, skipping duplicate call\n');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
this.isShuttingDown = true;
|
|
39
|
+
process.stderr.write('[RequestScope] Starting graceful shutdown...\n');
|
|
40
|
+
const startTime = Date.now();
|
|
41
|
+
try {
|
|
42
|
+
// 1. Stop retention scheduler
|
|
43
|
+
if (this.resources.retentionScheduler) {
|
|
44
|
+
process.stderr.write('[RequestScope] Stopping retention scheduler...\n');
|
|
45
|
+
this.resources.retentionScheduler.stop();
|
|
46
|
+
}
|
|
47
|
+
// 2. Drain queue with timeout
|
|
48
|
+
if (this.resources.queue) {
|
|
49
|
+
process.stderr.write(`[RequestScope] Draining queue (timeout: ${drainTimeoutMs}ms)...\n`);
|
|
50
|
+
try {
|
|
51
|
+
await Promise.race([
|
|
52
|
+
this.resources.queue.drain(drainTimeoutMs),
|
|
53
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Queue drain timeout')), drainTimeoutMs))
|
|
54
|
+
]);
|
|
55
|
+
process.stderr.write('[RequestScope] Queue drained successfully\n');
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
59
|
+
process.stderr.write(`[RequestScope] Queue drain warning: ${message}\n`);
|
|
60
|
+
}
|
|
61
|
+
// 3. Destroy queue (clears timer)
|
|
62
|
+
process.stderr.write('[RequestScope] Destroying queue...\n');
|
|
63
|
+
this.resources.queue.destroy();
|
|
64
|
+
}
|
|
65
|
+
// 4. Close all database connections
|
|
66
|
+
process.stderr.write('[RequestScope] Closing database connections...\n');
|
|
67
|
+
await adapterSingleton.closeAll();
|
|
68
|
+
process.stderr.write('[RequestScope] Database connections closed\n');
|
|
69
|
+
const duration = Date.now() - startTime;
|
|
70
|
+
process.stderr.write(`[RequestScope] Graceful shutdown completed in ${duration}ms\n`);
|
|
71
|
+
// 5. Log remaining active handles for debugging
|
|
72
|
+
if (logHandles) {
|
|
73
|
+
this.logActiveHandles();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
78
|
+
process.stderr.write(`[RequestScope] Shutdown error: ${message}\n`);
|
|
79
|
+
// Log active handles even on error
|
|
80
|
+
if (logHandles) {
|
|
81
|
+
this.logActiveHandles();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Logs active handles to help identify what's preventing process exit.
|
|
87
|
+
*/
|
|
88
|
+
logActiveHandles() {
|
|
89
|
+
try {
|
|
90
|
+
// @ts-ignore - _getActiveHandles is not in TypeScript definitions
|
|
91
|
+
const handles = process._getActiveHandles();
|
|
92
|
+
process.stderr.write(`[RequestScope] Active handles (${handles.length}):\n`);
|
|
93
|
+
for (let i = 0; i < Math.min(handles.length, 20); i++) {
|
|
94
|
+
const handle = handles[i];
|
|
95
|
+
const type = handle.constructor.name;
|
|
96
|
+
// @ts-ignore
|
|
97
|
+
const details = handle._handle?.type || '';
|
|
98
|
+
process.stderr.write(` [${i}] ${type}${details ? ` (${details})` : ''}\n`);
|
|
99
|
+
}
|
|
100
|
+
if (handles.length > 20) {
|
|
101
|
+
process.stderr.write(` ... and ${handles.length - 20} more\n`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
process.stderr.write('[RequestScope] Could not log active handles\n');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Returns whether shutdown is currently in progress.
|
|
110
|
+
*/
|
|
111
|
+
isShutdownInProgress() {
|
|
112
|
+
return this.isShuttingDown;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Export singleton instance
|
|
116
|
+
export const shutdownManager = new ShutdownManager();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdapterSingleton — Manages singleton database adapter instances.
|
|
3
|
+
*
|
|
4
|
+
* Ensures that only one database connection/pool exists per unique configuration
|
|
5
|
+
* across the entire application lifecycle. This prevents:
|
|
6
|
+
* - Multiple connection pools when middleware is called multiple times
|
|
7
|
+
* - Duplicate connections when setup function is imported multiple times
|
|
8
|
+
* - Connection leaks on server restarts in dev mode (nodemon)
|
|
9
|
+
*
|
|
10
|
+
* The singleton is keyed by a hash of the storage configuration, so different
|
|
11
|
+
* configurations will have separate connections (as expected).
|
|
12
|
+
*/
|
|
13
|
+
import type { StorageAdapter, StorageConfig } from './types.js';
|
|
14
|
+
declare class AdapterSingletonManager {
|
|
15
|
+
private instances;
|
|
16
|
+
/**
|
|
17
|
+
* Gets or creates a singleton adapter instance for the given configuration.
|
|
18
|
+
*
|
|
19
|
+
* @param config - Storage configuration
|
|
20
|
+
* @returns StorageAdapter instance (shared if already exists)
|
|
21
|
+
*/
|
|
22
|
+
getOrCreate(config: StorageConfig): StorageAdapter;
|
|
23
|
+
/**
|
|
24
|
+
* Marks an adapter as initialized.
|
|
25
|
+
* This is called after successful adapter.initialize().
|
|
26
|
+
*/
|
|
27
|
+
markInitialized(adapter: StorageAdapter, config: StorageConfig): void;
|
|
28
|
+
/**
|
|
29
|
+
* Decrements reference count for an adapter.
|
|
30
|
+
* When reference count reaches zero, the adapter is cleaned up.
|
|
31
|
+
*
|
|
32
|
+
* @param adapter - StorageAdapter instance to release
|
|
33
|
+
* @param config - Storage configuration used to create the adapter
|
|
34
|
+
*/
|
|
35
|
+
release(adapter: StorageAdapter, config: StorageConfig): void;
|
|
36
|
+
/**
|
|
37
|
+
* Gets the current number of singleton instances (for debugging).
|
|
38
|
+
*/
|
|
39
|
+
getInstanceCount(): number;
|
|
40
|
+
/**
|
|
41
|
+
* Clears all singleton instances.
|
|
42
|
+
* This should only be used in tests or on application shutdown.
|
|
43
|
+
*/
|
|
44
|
+
clearAll(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Closes all adapter connections and clears the singleton instances.
|
|
47
|
+
* This should be called during application graceful shutdown.
|
|
48
|
+
*/
|
|
49
|
+
closeAll(): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
export declare const adapterSingleton: AdapterSingletonManager;
|
|
52
|
+
export {};
|