@cldmv/slothlet 2.7.0 → 2.7.1

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 CHANGED
@@ -522,10 +522,19 @@ Returns true if the API is loaded.
522
522
 
523
523
  #### `slothlet.shutdown()` ⇒ `Promise<void>`
524
524
 
525
- Gracefully shuts down the API and cleans up resources.
525
+ Gracefully shuts down the API and performs comprehensive resource cleanup to prevent hanging processes.
526
+
527
+ **Cleanup includes:**
528
+ - Hook manager state and registered hooks
529
+ - AsyncLocalStorage context and bindings
530
+ - EventEmitter listeners and AsyncResource instances (including third-party libraries)
531
+ - Instance data and runtime coordination
526
532
 
527
533
  **Returns:** `Promise<void>` - Resolves when shutdown is complete
528
534
 
535
+ > [!IMPORTANT]
536
+ > **🛡️ Process Cleanup**: The shutdown method now performs comprehensive cleanup of all EventEmitter listeners created after slothlet loads, including those from third-party libraries like pg-pool. This prevents hanging AsyncResource instances that could prevent your Node.js process from exiting cleanly.
537
+
529
538
  > [!NOTE]
530
539
  > **📚 For detailed API documentation with comprehensive parameter descriptions, method signatures, and examples, see [docs/API.md](https://github.com/CLDMV/slothlet/blob/HEAD/docs/API.md)**
531
540
 
@@ -649,6 +658,7 @@ console.log("TCP server started with context preservation");
649
658
  - ✅ **Nested Events**: Works with any depth of EventEmitter nesting (server → socket → custom emitters)
650
659
  - ✅ **Universal Support**: All EventEmitter methods (`on`, `once`, `addListener`) are automatically context-aware
651
660
  - ✅ **Production Ready**: Uses Node.js AsyncResource patterns for reliable context propagation
661
+ - ✅ **Clean Shutdown**: Automatically cleans up all AsyncResource instances during shutdown to prevent hanging processes
652
662
  - ✅ **Zero Overhead**: Only wraps listeners when context is active, minimal performance impact
653
663
 
654
664
  > [!TIP]
@@ -26,6 +26,17 @@ import { AsyncLocalStorage } from "node:async_hooks";
26
26
  const defaultALS = new AsyncLocalStorage();
27
27
 
28
28
 
29
+ let originalMethods = null;
30
+
31
+
32
+ const globalResourceSet = new Set();
33
+
34
+
35
+
36
+ const globalListenerTracker = new WeakMap();
37
+ const allPatchedListeners = new Set();
38
+
39
+
29
40
  export function enableAlsForEventEmitters(als = defaultALS) {
30
41
 
31
42
  const kPatched = Symbol.for("slothlet.als.patched");
@@ -52,6 +63,9 @@ export function enableAlsForEventEmitters(als = defaultALS) {
52
63
  const resource = new AsyncResource("slothlet-als-listener");
53
64
 
54
65
 
66
+ globalResourceSet.add(resource);
67
+
68
+
55
69
  const runtime_wrappedListener = function (...args) {
56
70
  return resource.runInAsyncScope(
57
71
  () => {
@@ -62,6 +76,9 @@ export function enableAlsForEventEmitters(als = defaultALS) {
62
76
  );
63
77
  };
64
78
 
79
+
80
+ runtime_wrappedListener._slothletResource = resource;
81
+
65
82
  return runtime_wrappedListener;
66
83
  }
67
84
 
@@ -81,6 +98,22 @@ export function enableAlsForEventEmitters(als = defaultALS) {
81
98
  proto[addFnName] = function (event, listener) {
82
99
  const map = runtime_ensureMap(this);
83
100
  const wrapped = runtime_wrapListener(listener);
101
+
102
+
103
+
104
+ if (!globalListenerTracker.has(this)) {
105
+ globalListenerTracker.set(this, new Set());
106
+ }
107
+ const listenerInfo = {
108
+ emitter: this,
109
+ event,
110
+ originalListener: listener,
111
+ wrappedListener: wrapped,
112
+ addMethod: addFnName
113
+ };
114
+ globalListenerTracker.get(this).add(listenerInfo);
115
+ allPatchedListeners.add(listenerInfo);
116
+
84
117
  if (wrapped !== listener) map.set(listener, wrapped);
85
118
  return orig.call(this, event, wrapped);
86
119
  };
@@ -99,6 +132,30 @@ export function enableAlsForEventEmitters(als = defaultALS) {
99
132
  const runtime_removeWrapper = function (event, listener) {
100
133
  const map = runtime_ensureMap(this);
101
134
  const wrapped = map.get(listener) || listener;
135
+
136
+
137
+ if (globalListenerTracker.has(this)) {
138
+ const emitterListeners = globalListenerTracker.get(this);
139
+ for (const info of emitterListeners) {
140
+ if (info.originalListener === listener || info.wrappedListener === wrapped) {
141
+ emitterListeners.delete(info);
142
+ allPatchedListeners.delete(info);
143
+ break;
144
+ }
145
+ }
146
+ }
147
+
148
+
149
+ if (wrapped && wrapped._slothletResource) {
150
+ const resource = wrapped._slothletResource;
151
+ globalResourceSet.delete(resource);
152
+ try {
153
+ resource.emitDestroy();
154
+ } catch (err) {
155
+
156
+ }
157
+ }
158
+
102
159
  map.delete(listener);
103
160
  return method.call(this, event, wrapped);
104
161
  };
@@ -117,4 +174,84 @@ export function enableAlsForEventEmitters(als = defaultALS) {
117
174
  if (this[kMap]) this[kMap] = new WeakMap();
118
175
  return res;
119
176
  };
177
+
178
+
179
+ if (!originalMethods) {
180
+ originalMethods = {
181
+ on: origOn,
182
+ once: origOnce,
183
+ addListener: origAdd,
184
+ prependListener: origPre,
185
+ prependOnceListener: origPreO,
186
+ off: origOff,
187
+ removeListener: origRem,
188
+ removeAllListeners: origRemoveAll
189
+ };
190
+ }
191
+ }
192
+
193
+
194
+
195
+ export function cleanupAllSlothletListeners() {
196
+ let cleanedCount = 0;
197
+ let errorCount = 0;
198
+
199
+
200
+ for (const listenerInfo of allPatchedListeners) {
201
+ try {
202
+ const { emitter, event, wrappedListener } = listenerInfo;
203
+ if (emitter && typeof emitter.removeListener === "function") {
204
+ emitter.removeListener(event, wrappedListener);
205
+ cleanedCount++;
206
+ }
207
+ } catch (err) {
208
+ errorCount++;
209
+
210
+ }
211
+ }
212
+
213
+
214
+ allPatchedListeners.clear();
215
+
216
+
217
+ if (process.env.NODE_ENV === "development" || process.env.SLOTHLET_DEBUG) {
218
+ console.log(`[slothlet] Cleaned up ${cleanedCount} listeners (${errorCount} errors)`);
219
+ }
220
+ }
221
+
222
+ export function disableAlsForEventEmitters() {
223
+ const kPatched = Symbol.for("slothlet.als.patched");
224
+
225
+ if (!EventEmitter.prototype[kPatched] || !originalMethods) return;
226
+
227
+
228
+ cleanupAllSlothletListeners();
229
+
230
+
231
+ for (const resource of globalResourceSet) {
232
+ try {
233
+ resource.emitDestroy();
234
+ } catch (err) {
235
+
236
+ console.warn("[slothlet] AsyncResource cleanup warning:", err.message);
237
+ }
238
+ }
239
+ globalResourceSet.clear();
240
+
241
+
242
+ const proto = EventEmitter.prototype;
243
+ proto.on = originalMethods.on;
244
+ proto.once = originalMethods.once;
245
+ proto.addListener = originalMethods.addListener;
246
+ if (originalMethods.prependListener) proto.prependListener = originalMethods.prependListener;
247
+ if (originalMethods.prependOnceListener) proto.prependOnceListener = originalMethods.prependOnceListener;
248
+ if (originalMethods.off) proto.off = originalMethods.off;
249
+ proto.removeListener = originalMethods.removeListener;
250
+ proto.removeAllListeners = originalMethods.removeAllListeners;
251
+
252
+
253
+ delete EventEmitter.prototype[kPatched];
254
+
255
+
256
+ originalMethods = null;
120
257
  }
@@ -53,6 +53,14 @@ export class HookManager {
53
53
  }
54
54
 
55
55
 
56
+ cleanup() {
57
+ this.hooks.clear();
58
+ this.reportedErrors = new WeakSet();
59
+ this.registrationOrder = 0;
60
+ this.enabled = false;
61
+ }
62
+
63
+
56
64
  off(nameOrPattern) {
57
65
 
58
66
  if (this.hooks.has(nameOrPattern)) {
package/dist/slothlet.mjs CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  buildCategoryDecisions
32
32
  } from "@cldmv/slothlet/helpers/api_builder";
33
33
  import { updateInstanceData, cleanupInstance } from "./lib/helpers/instance-manager.mjs";
34
+ import { disableAlsForEventEmitters, cleanupAllSlothletListeners } from "./lib/helpers/als-eventemitter.mjs";
34
35
  import { HookManager } from "./lib/helpers/hooks.mjs";
35
36
 
36
37
 
@@ -1152,12 +1153,30 @@ const slothletObject = {
1152
1153
  this._boundAPIShutdown = null;
1153
1154
 
1154
1155
 
1156
+ if (this.hookManager) {
1157
+ this.hookManager.cleanup();
1158
+ this.hookManager = null;
1159
+ }
1160
+
1161
+
1155
1162
 
1156
1163
 
1157
1164
  if (this.instanceId) {
1158
1165
  await cleanupInstance(this.instanceId);
1159
1166
  }
1160
1167
 
1168
+
1169
+
1170
+ try {
1171
+
1172
+ cleanupAllSlothletListeners();
1173
+
1174
+ disableAlsForEventEmitters();
1175
+ } catch (cleanupError) {
1176
+
1177
+ console.warn("[slothlet] Warning: EventEmitter cleanup failed:", cleanupError.message);
1178
+ }
1179
+
1161
1180
  if (apiError || internalError) throw apiError || internalError;
1162
1181
  }
1163
1182
  } finally {
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@cldmv/slothlet",
3
- "version": "2.7.0",
3
+ "version": "2.7.1",
4
4
  "moduleVersions": {
5
- "lazy": "1.3.0",
6
- "eager": "1.3.0"
5
+ "lazy": "1.3.1",
6
+ "eager": "1.3.1"
7
7
  },
8
8
  "description": "Slothlet: Modular API Loader for Node.js. Lazy mode dynamically loads API modules and submodules only when accessed, supporting both lazy and eager loading.",
9
9
  "main": "./index.cjs",
@@ -19,5 +19,38 @@
19
19
  * enableAlsForEventEmitters(als);
20
20
  */
21
21
  export function enableAlsForEventEmitters(als?: AsyncLocalStorage<any>): void;
22
+ /**
23
+ * Disable AsyncLocalStorage context propagation for EventEmitter instances.
24
+ *
25
+ * @function disableAlsForEventEmitters
26
+ * @package
27
+ *
28
+ * @description
29
+ * Restores original EventEmitter methods, removing the AsyncLocalStorage
30
+ * context propagation. This should be called during cleanup to prevent
31
+ * hanging AsyncResource instances that can keep the event loop alive.
32
+ *
33
+ * @example
34
+ * // Disable ALS patching during shutdown
35
+ * disableAlsForEventEmitters();
36
+ */
37
+ /**
38
+ * Clean up ALL listeners that went through slothlet's EventEmitter patching.
39
+ *
40
+ * @function cleanupAllSlothletListeners
41
+ * @package
42
+ *
43
+ * @description
44
+ * Removes all event listeners that were registered through slothlet's patched
45
+ * EventEmitter methods. This includes listeners from third-party libraries
46
+ * that got wrapped with AsyncResource instances. This nuclear cleanup option
47
+ * should be called during shutdown to prevent hanging listeners.
48
+ *
49
+ * @example
50
+ * // Clean up all patched listeners during shutdown
51
+ * cleanupAllSlothletListeners();
52
+ */
53
+ export function cleanupAllSlothletListeners(): void;
54
+ export function disableAlsForEventEmitters(): void;
22
55
  import { AsyncLocalStorage } from "node:async_hooks";
23
56
  //# sourceMappingURL=als-eventemitter.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"als-eventemitter.d.mts","sourceRoot":"","sources":["../../../../dist/lib/helpers/als-eventemitter.mjs"],"names":[],"mappings":"AAuCA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,8EAqJC;kCA9KiC,kBAAkB"}
1
+ {"version":3,"file":"als-eventemitter.d.mts","sourceRoot":"","sources":["../../../../dist/lib/helpers/als-eventemitter.mjs"],"names":[],"mappings":"AAkDA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,8EAiNC;AAED;;;;;;;;;;;;;;GAcG;AACH;;;;;;;;;;;;;;;GAeG;AACH,oDAyBC;AAED,mDAmCC;kCApViC,kBAAkB"}
@@ -55,6 +55,18 @@ export class HookManager {
55
55
  priority?: number;
56
56
  pattern?: string;
57
57
  }): string;
58
+ /**
59
+ * Clean up all hooks and resources
60
+ * @public
61
+ * @description
62
+ * Clears all registered hooks and resets internal state.
63
+ * Should be called during shutdown to prevent memory leaks.
64
+ *
65
+ * @example
66
+ * // Clean up during shutdown
67
+ * manager.cleanup();
68
+ */
69
+ public cleanup(): void;
58
70
  /**
59
71
  * @function off
60
72
  * @public
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.mts","sourceRoot":"","sources":["../../../../dist/lib/helpers/hooks.mjs"],"names":[],"mappings":"AAgCA;;;;;;;;;;;;;GAaG;AACH;IACC;;;;;;OAMG;IACH,sBALW,OAAO,mBACP,MAAM,YAEd;QAA0B,cAAc,GAAhC,OAAO;KACjB,EAQA;IANA,iBAAsB;IACtB,uBAAoC;IACpC,wBAAqD;IACrD,qBAAsB;IACtB,0BAA0B;IAC1B,gCAAmC;IAGpC;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,gBAnBW,MAAM,QACN,MAAM,+BAOd;QAAyB,QAAQ,GAAzB,MAAM;QACW,OAAO,GAAxB,MAAM;KACd,GAAU,MAAM,CA0BlB;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,0BAdW,MAAM,GACJ,OAAO,CA6BnB;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,oBAdW,MAAM,GACJ,IAAI,CA0BhB;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,mBAfW,MAAM,GACJ,KAAK,CAAC,MAAM,CAAC,CA4BzB;IAED;;;;;;;;;;;;OAYG;IACH,wBAVW,MAAM,GACJ,IAAI,CAchB;IAED;;;;;;;;;;;OAWG;IACH,kBATa,IAAI,CAWhB;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,2BAkCC;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,0BAwBC;IAED;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,2BAqBC;IAED;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,0BAyBC;IAED;;;;;;;;;;;;;;OAcG;IACH,0BAkBC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,wBAmBC;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,sBA4CC;IAED;;;;;;;;;;;;;OAaG;IACH,2BAuBC;IAED;;;;;;;;;;;;;OAaG;IACH,wBAiBC;IAED;;;;;;;;;;;;;;OAcG;IACH,sBAOC;CACD"}
1
+ {"version":3,"file":"hooks.d.mts","sourceRoot":"","sources":["../../../../dist/lib/helpers/hooks.mjs"],"names":[],"mappings":"AAgCA;;;;;;;;;;;;;GAaG;AACH;IACC;;;;;;OAMG;IACH,sBALW,OAAO,mBACP,MAAM,YAEd;QAA0B,cAAc,GAAhC,OAAO;KACjB,EAQA;IANA,iBAAsB;IACtB,uBAAoC;IACpC,wBAAqD;IACrD,qBAAsB;IACtB,0BAA0B;IAC1B,gCAAmC;IAGpC;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,gBAnBW,MAAM,QACN,MAAM,+BAOd;QAAyB,QAAQ,GAAzB,MAAM;QACW,OAAO,GAAxB,MAAM;KACd,GAAU,MAAM,CA0BlB;IAED;;;;;;;;;;OAUG;IACH,uBAKC;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,0BAdW,MAAM,GACJ,OAAO,CA6BnB;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,oBAdW,MAAM,GACJ,IAAI,CA0BhB;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,mBAfW,MAAM,GACJ,KAAK,CAAC,MAAM,CAAC,CA4BzB;IAED;;;;;;;;;;;;OAYG;IACH,wBAVW,MAAM,GACJ,IAAI,CAchB;IAED;;;;;;;;;;;OAWG;IACH,kBATa,IAAI,CAWhB;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,2BAkCC;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,0BAwBC;IAED;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,2BAqBC;IAED;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,0BAyBC;IAED;;;;;;;;;;;;;;OAcG;IACH,0BAkBC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,wBAmBC;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,sBA4CC;IAED;;;;;;;;;;;;;OAaG;IACH,2BAuBC;IAED;;;;;;;;;;;;;OAaG;IACH,wBAiBC;IAED;;;;;;;;;;;;;;OAcG;IACH,sBAOC;CACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"slothlet.d.mts","sourceRoot":"","sources":["../../dist/slothlet.mjs"],"names":[],"mappings":"AA+hDA;;;;;;;;;GASG;AACH,kDARW,WAAS,MAAM,UACf,WAAS,MAAM,QAwCzB;AAp5CD;;;;;;;GAOG;AACH,mBAJU,MAAM,CAIO;AAEvB;;;;;GAKG;AACH,sBAJU,MAAM,CAIU;AAE1B;;;;;GAKG;AACH,wBAJU,MAAM,CAIY;;;;;;;;;UAu4Cd,MAAM;;;;;;WAIN,OAAO;;;;;;;;WAGP,MAAM;;;;;;;;aAKN,MAAM;;;;;;;cAKN,MAAM;;;;;;;eAIN,MAAM;;;;;;;;YAIN,OAAO;;;;;;;eAKP,MAAM;;;;;;cAIN,MAAM;;;;;;gBAGN,MAAM;;;;;;eAMjB;QAA8B,UAAU,GAA7B,OAAO;QACY,gBAAgB,GAAnC,OAAO;QACY,gBAAgB,GAAnC,OAAO;QACW,KAAK,GAClC;YAAqC,KAAK,GAA/B,MAAM,EAAE;YACkB,gBAAgB,GAA1C,MAAM,EAAE;YACkB,KAAK,GAA/B,MAAM,EAAE;YACkB,KAAK,GAA/B,MAAM,EAAE;SACrB;KAAA;;AAx7CD;;;;;;;;GAQG;AACH,mCAJW,eAAe,GACb,OAAO,CAAC,WAAS,MAAM,CAAC,CAiCpC"}
1
+ {"version":3,"file":"slothlet.d.mts","sourceRoot":"","sources":["../../dist/slothlet.mjs"],"names":[],"mappings":"AAkjDA;;;;;;;;;GASG;AACH,kDARW,WAAS,MAAM,UACf,WAAS,MAAM,QAwCzB;AAt6CD;;;;;;;GAOG;AACH,mBAJU,MAAM,CAIO;AAEvB;;;;;GAKG;AACH,sBAJU,MAAM,CAIU;AAE1B;;;;;GAKG;AACH,wBAJU,MAAM,CAIY;;;;;;;;;UAy5Cd,MAAM;;;;;;WAIN,OAAO;;;;;;;;WAGP,MAAM;;;;;;;;aAKN,MAAM;;;;;;;cAKN,MAAM;;;;;;;eAIN,MAAM;;;;;;;;YAIN,OAAO;;;;;;;eAKP,MAAM;;;;;;cAIN,MAAM;;;;;;gBAGN,MAAM;;;;;;eAMjB;QAA8B,UAAU,GAA7B,OAAO;QACY,gBAAgB,GAAnC,OAAO;QACY,gBAAgB,GAAnC,OAAO;QACW,KAAK,GAClC;YAAqC,KAAK,GAA/B,MAAM,EAAE;YACkB,gBAAgB,GAA1C,MAAM,EAAE;YACkB,KAAK,GAA/B,MAAM,EAAE;YACkB,KAAK,GAA/B,MAAM,EAAE;SACrB;KAAA;;AA18CD;;;;;;;;GAQG;AACH,mCAJW,eAAe,GACb,OAAO,CAAC,WAAS,MAAM,CAAC,CAiCpC"}