@cldmv/slothlet 2.6.3 → 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
@@ -74,6 +74,15 @@ v2.0 represents a ground-up rewrite with enterprise-grade features:
74
74
  - **AsyncResource Integration**: Production-ready context management following Node.js best practices
75
75
  - **Zero Configuration**: Works automatically with TCP servers, HTTP servers, and any EventEmitter-based patterns
76
76
 
77
+ ### 🎣 **Hook System (v2.6.4)** ⭐ NEW
78
+
79
+ - **3-Hook Types**: `before` (modify args or cancel), `after` (transform results), `always` (observe final result)
80
+ - **Cross-Mode Compatibility**: Works seamlessly across all 4 combinations (eager/lazy × async/live)
81
+ - **Pattern Matching**: Target specific functions or use wildcards (`math.*`, `*.add`, `**`)
82
+ - **Priority Control**: Order hook execution with numeric priorities
83
+ - **Runtime Control**: Enable/disable hooks at runtime, globally or by pattern
84
+ - **Short-Circuit Support**: Cancel execution and return custom values from `before` hooks
85
+
77
86
  ---
78
87
 
79
88
  ## 🚀 Key Features
@@ -513,10 +522,19 @@ Returns true if the API is loaded.
513
522
 
514
523
  #### `slothlet.shutdown()` ⇒ `Promise<void>`
515
524
 
516
- 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
517
532
 
518
533
  **Returns:** `Promise<void>` - Resolves when shutdown is complete
519
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
+
520
538
  > [!NOTE]
521
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)**
522
540
 
@@ -640,6 +658,7 @@ console.log("TCP server started with context preservation");
640
658
  - ✅ **Nested Events**: Works with any depth of EventEmitter nesting (server → socket → custom emitters)
641
659
  - ✅ **Universal Support**: All EventEmitter methods (`on`, `once`, `addListener`) are automatically context-aware
642
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
643
662
  - ✅ **Zero Overhead**: Only wraps listeners when context is active, minimal performance impact
644
663
 
645
664
  > [!TIP]
@@ -712,6 +731,488 @@ console.log("Processing completed with context preservation");
712
731
  > [!TIP]
713
732
  > **Universal Class Support**: Any class instance returned from your API functions automatically maintains slothlet context. This includes database models, service classes, utility classes, and any other object-oriented patterns in your codebase.
714
733
 
734
+ ### Hook System
735
+
736
+ Slothlet provides a powerful hook system for intercepting, modifying, and observing API function calls. Hooks work seamlessly across all loading modes (eager/lazy) and runtime types (async/live).
737
+
738
+ #### Hook Configuration
739
+
740
+ Hooks can be configured when creating a slothlet instance:
741
+
742
+ ```javascript
743
+ // Enable hooks (simple boolean)
744
+ const api = await slothlet({
745
+ dir: "./api",
746
+ hooks: true // Enable all hooks with default pattern "**"
747
+ });
748
+
749
+ // Enable with custom pattern
750
+ const api = await slothlet({
751
+ dir: "./api",
752
+ hooks: "database.*" // Only enable for database functions
753
+ });
754
+
755
+ // Full configuration object
756
+ const api = await slothlet({
757
+ dir: "./api",
758
+ hooks: {
759
+ enabled: true,
760
+ pattern: "**", // Default pattern for filtering
761
+ suppressErrors: false // Control error throwing behavior
762
+ }
763
+ });
764
+ ```
765
+
766
+ **Configuration Options:**
767
+
768
+ - **`enabled`** (boolean): Enable or disable hook execution
769
+ - **`pattern`** (string): Default pattern for filtering which functions hooks apply to
770
+ - **`suppressErrors`** (boolean): Control error throwing behavior
771
+ - `false` (default): Errors are sent to error hooks, THEN thrown (normal behavior)
772
+ - `true`: Errors are sent to error hooks, BUT NOT thrown (returns `undefined`)
773
+
774
+ **Error Suppression Behavior:**
775
+
776
+ Error hooks **ALWAYS receive errors** regardless of this setting. The `suppressErrors` option only controls whether errors are thrown after error hooks execute.
777
+
778
+ > [!IMPORTANT]
779
+ > **Hooks Must Be Enabled**: Error hooks (and all hooks) only execute when `hooks.enabled: true`. If hooks are disabled, errors are thrown normally without any hook execution.
780
+
781
+ When `suppressErrors: true`, errors are caught and sent to error hooks, but not thrown:
782
+
783
+ ```javascript
784
+ const api = await slothlet({
785
+ dir: "./api",
786
+ hooks: {
787
+ enabled: true,
788
+ pattern: "**",
789
+ suppressErrors: true // Suppress all errors
790
+ }
791
+ });
792
+
793
+ // Register error hook to monitor failures
794
+ api.hooks.on(
795
+ "error-monitor",
796
+ "error",
797
+ ({ path, error, source }) => {
798
+ console.error(`Error in ${path}:`, error.message);
799
+ // Log to monitoring service without crashing
800
+ },
801
+ { pattern: "**" }
802
+ );
803
+
804
+ // Function errors won't crash the application
805
+ const result = await api.riskyOperation();
806
+ if (result === undefined) {
807
+ // Function failed but didn't throw
808
+ console.log("Operation failed gracefully");
809
+ }
810
+ ```
811
+
812
+ **Error Flow:**
813
+
814
+ 1. Error occurs (in before hook, function, or after hook)
815
+ 2. Error hooks execute and receive the error
816
+ 3. **If `suppressErrors: false`** → Error is thrown (crashes if uncaught)
817
+ 4. **If `suppressErrors: true`** → Error is NOT thrown, function returns `undefined`
818
+
819
+ **What Gets Suppressed (when `suppressErrors: true`):**
820
+
821
+ - ✅ Before hook errors → Sent to error hooks, NOT thrown
822
+ - ✅ Function execution errors → Sent to error hooks, NOT thrown
823
+ - ✅ After hook errors → Sent to error hooks, NOT thrown
824
+ - ✅ Always hook errors → Sent to error hooks, never thrown (regardless of setting)
825
+
826
+ > [!TIP]
827
+ > **Use Case**: Enable `suppressErrors: true` for resilient systems where you want to monitor failures without crashing. Perfect for background workers, batch processors, or systems with comprehensive error monitoring.
828
+
829
+ > [!CAUTION]
830
+ > **Critical Operations**: For validation or authorization hooks where errors MUST stop execution, use `suppressErrors: false` (default) to ensure errors propagate normally.
831
+
832
+ #### Hook Types
833
+
834
+ **Four hook types with distinct responsibilities:**
835
+
836
+ - **`before`**: Intercept before function execution
837
+ - Modify arguments passed to functions
838
+ - Cancel execution and return custom values (short-circuit)
839
+ - Execute validation or logging before function runs
840
+ - **`after`**: Transform results after successful execution
841
+ - Transform function return values
842
+ - Only runs if function executes (skipped on short-circuit)
843
+ - Chain multiple transformations in priority order
844
+ - **`always`**: Observe final result with full execution context
845
+ - Always executes after function completes
846
+ - Runs even when `before` hooks cancel execution or errors occur
847
+ - Receives complete context: `{ path, result, hasError, errors }`
848
+ - Cannot modify result (read-only observation)
849
+ - Perfect for unified logging of both success and error scenarios
850
+ - **`error`**: Monitor and handle errors
851
+ - Receives detailed error context with source tracking
852
+ - Error source types: 'before', 'function', 'after', 'always', 'unknown'
853
+ - Includes error type, hook ID, hook tag, timestamp, and stack trace
854
+ - Perfect for error monitoring, logging, and alerting
855
+
856
+ #### Basic Usage
857
+
858
+ ```javascript
859
+ import slothlet from "@cldmv/slothlet";
860
+
861
+ const api = await slothlet({
862
+ dir: "./api",
863
+ hooks: true // Enable hooks
864
+ });
865
+
866
+ // Before hook: Modify arguments
867
+ api.hooks.on(
868
+ "validate-input",
869
+ "before",
870
+ ({ path, args }) => {
871
+ console.log(`Calling ${path} with args:`, args);
872
+ // Return modified args or original
873
+ return [args[0] * 2, args[1] * 2];
874
+ },
875
+ { pattern: "math.add", priority: 100 }
876
+ );
877
+
878
+ // After hook: Transform result
879
+ api.hooks.on(
880
+ "format-output",
881
+ "after",
882
+ ({ path, result }) => {
883
+ console.log(`${path} returned:`, result);
884
+ // Return transformed result
885
+ return result * 10;
886
+ },
887
+ { pattern: "math.*", priority: 100 }
888
+ );
889
+
890
+ // Always hook: Observe final result with error context
891
+ api.hooks.on(
892
+ "log-execution",
893
+ "always",
894
+ ({ path, result, hasError, errors }) => {
895
+ if (hasError) {
896
+ console.log(`${path} failed with ${errors.length} error(s):`, errors);
897
+ } else {
898
+ console.log(`${path} succeeded with result:`, result);
899
+ }
900
+ // Return value ignored - read-only observer
901
+ },
902
+ { pattern: "**" } // All functions
903
+ );
904
+
905
+ // Call function - hooks execute automatically
906
+ const result = await api.math.add(2, 3);
907
+ // Logs: "Calling math.add with args: [2, 3]"
908
+ // Logs: "math.add returned: 10" (4+6)
909
+ // Logs: "Final result for math.add: 100" (10*10)
910
+ // result === 100
911
+ ```
912
+
913
+ #### Short-Circuit Execution
914
+
915
+ `before` hooks can cancel function execution and return custom values:
916
+
917
+ ```javascript
918
+ // Caching hook example
919
+ const cache = new Map();
920
+
921
+ api.hooks.on(
922
+ "cache-check",
923
+ "before",
924
+ ({ path, args }) => {
925
+ const key = JSON.stringify({ path, args });
926
+ if (cache.has(key)) {
927
+ console.log(`Cache hit for ${path}`);
928
+ return cache.get(key); // Short-circuit: return cached value
929
+ }
930
+ // Return undefined to continue to function
931
+ },
932
+ { pattern: "**", priority: 1000 } // High priority
933
+ );
934
+
935
+ api.hooks.on(
936
+ "cache-store",
937
+ "after",
938
+ ({ path, args, result }) => {
939
+ const key = JSON.stringify({ path, args });
940
+ cache.set(key, result);
941
+ return result; // Pass through
942
+ },
943
+ { pattern: "**", priority: 100 }
944
+ );
945
+
946
+ // First call - executes function and caches
947
+ await api.math.add(2, 3); // Computes and stores
948
+
949
+ // Second call - returns cached value (function not executed)
950
+ await api.math.add(2, 3); // Cache hit! No computation
951
+ ```
952
+
953
+ #### Pattern Matching
954
+
955
+ Hooks support flexible pattern matching:
956
+
957
+ ```javascript
958
+ // Exact match
959
+ api.hooks.on("hook1", "before", handler, { pattern: "math.add" });
960
+
961
+ // Wildcard: all functions in namespace
962
+ api.hooks.on("hook2", "before", handler, { pattern: "math.*" });
963
+
964
+ // Wildcard: specific function in all namespaces
965
+ api.hooks.on("hook3", "before", handler, { pattern: "*.add" });
966
+
967
+ // Global: all functions
968
+ api.hooks.on("hook4", "before", handler, { pattern: "**" });
969
+ ```
970
+
971
+ #### Priority and Chaining
972
+
973
+ Multiple hooks execute in priority order (highest first):
974
+
975
+ ```javascript
976
+ // High priority - runs first
977
+ api.hooks.on(
978
+ "validate",
979
+ "before",
980
+ ({ args }) => {
981
+ if (args[0] < 0) throw new Error("Negative numbers not allowed");
982
+ return args;
983
+ },
984
+ { pattern: "math.*", priority: 1000 }
985
+ );
986
+
987
+ // Medium priority - runs second
988
+ api.hooks.on("double", "before", ({ args }) => [args[0] * 2, args[1] * 2], { pattern: "math.*", priority: 500 });
989
+
990
+ // Low priority - runs last
991
+ api.hooks.on(
992
+ "log",
993
+ "before",
994
+ ({ path, args }) => {
995
+ console.log(`Final args for ${path}:`, args);
996
+ return args;
997
+ },
998
+ { pattern: "math.*", priority: 100 }
999
+ );
1000
+ ```
1001
+
1002
+ #### Runtime Control
1003
+
1004
+ Enable and disable hooks at runtime:
1005
+
1006
+ ```javascript
1007
+ const api = await slothlet({ dir: "./api", hooks: true });
1008
+
1009
+ // Add hooks
1010
+ api.hooks.on("test", "before", handler, { pattern: "math.*" });
1011
+
1012
+ // Disable all hooks
1013
+ api.hooks.disable();
1014
+ await api.math.add(2, 3); // No hooks execute
1015
+
1016
+ // Re-enable all hooks
1017
+ api.hooks.enable();
1018
+ await api.math.add(2, 3); // Hooks execute
1019
+
1020
+ // Enable specific pattern only
1021
+ api.hooks.disable();
1022
+ api.hooks.enable("math.*"); // Only math.* pattern enabled
1023
+ await api.math.add(2, 3); // math.* hooks execute
1024
+ await api.other.func(); // No hooks execute
1025
+ ```
1026
+
1027
+ #### Hook Management
1028
+
1029
+ ```javascript
1030
+ // List registered hooks
1031
+ const beforeHooks = api.hooks.list("before");
1032
+ const afterHooks = api.hooks.list("after");
1033
+ const allHooks = api.hooks.list(); // All types
1034
+
1035
+ // Remove specific hook by ID
1036
+ const id = api.hooks.on("temp", "before", handler, { pattern: "math.*" });
1037
+ api.hooks.off(id);
1038
+
1039
+ // Remove all hooks matching pattern
1040
+ api.hooks.off("math.*");
1041
+
1042
+ // Clear all hooks of a type
1043
+ api.hooks.clear("before"); // Remove all before hooks
1044
+ api.hooks.clear(); // Remove all hooks
1045
+ ```
1046
+
1047
+ #### Error Handling
1048
+
1049
+ Hooks have a special `error` type for observing function errors with detailed source tracking:
1050
+
1051
+ ```javascript
1052
+ api.hooks.on(
1053
+ "error-logger",
1054
+ "error",
1055
+ ({ path, error, source }) => {
1056
+ console.error(`Error in ${path}:`, error.message);
1057
+ console.error(`Source: ${source.type}`); // 'before', 'after', 'always', 'function', 'unknown'
1058
+
1059
+ if (source.type === "function") {
1060
+ console.error("Error occurred in function execution");
1061
+ } else if (["before", "after", "always"].includes(source.type)) {
1062
+ console.error(`Error occurred in ${source.type} hook:`);
1063
+ console.error(` Hook ID: ${source.hookId}`);
1064
+ console.error(` Hook Tag: ${source.hookTag}`);
1065
+ }
1066
+
1067
+ console.error(`Timestamp: ${source.timestamp}`);
1068
+ console.error(`Stack trace:\n${source.stack}`);
1069
+
1070
+ // Log to monitoring service with full context
1071
+ // Error is re-thrown after all error hooks execute
1072
+ },
1073
+ { pattern: "**" }
1074
+ );
1075
+
1076
+ try {
1077
+ await api.validateData({ invalid: true });
1078
+ } catch (error) {
1079
+ // Error hooks executed before this catch block
1080
+ console.log("Caught error:", error);
1081
+ }
1082
+ ```
1083
+
1084
+ ##### Error Source Tracking
1085
+
1086
+ Error hooks receive detailed context about where errors originated:
1087
+
1088
+ **Source Types:**
1089
+
1090
+ - `"function"`: Error occurred during function execution
1091
+ - `"before"`: Error occurred in a before hook
1092
+ - `"after"`: Error occurred in an after hook
1093
+ - `"always"`: Error occurred in an always hook
1094
+ - `"unknown"`: Error source could not be determined
1095
+
1096
+ **Source Metadata:**
1097
+
1098
+ - `source.type`: Error source type (see above)
1099
+ - `source.hookId`: Hook identifier (for hook errors)
1100
+ - `source.hookTag`: Hook tag/name (for hook errors)
1101
+ - `source.timestamp`: ISO timestamp when error occurred
1102
+ - `source.stack`: Full stack trace
1103
+
1104
+ **Example: Comprehensive Error Monitoring**
1105
+
1106
+ ```javascript
1107
+ const errorStats = {
1108
+ function: 0,
1109
+ before: 0,
1110
+ after: 0,
1111
+ always: 0,
1112
+ byHook: {}
1113
+ };
1114
+
1115
+ api.hooks.on(
1116
+ "error-analytics",
1117
+ "error",
1118
+ ({ path, error, source }) => {
1119
+ // Track error source statistics
1120
+ errorStats[source.type]++;
1121
+
1122
+ if (source.hookId) {
1123
+ if (!errorStats.byHook[source.hookTag]) {
1124
+ errorStats.byHook[source.hookTag] = 0;
1125
+ }
1126
+ errorStats.byHook[source.hookTag]++;
1127
+ }
1128
+
1129
+ // Log detailed error info
1130
+ console.error(`[${source.timestamp}] Error in ${path}:`);
1131
+ console.error(` Type: ${source.type}`);
1132
+ console.error(` Message: ${error.message}`);
1133
+
1134
+ if (source.type === "function") {
1135
+ // Function-level error - might be a bug in implementation
1136
+ console.error(" Action: Review function implementation");
1137
+ } else {
1138
+ // Hook-level error - might be a bug in hook logic
1139
+ console.error(` Action: Review ${source.hookTag} hook (${source.type})`);
1140
+ }
1141
+
1142
+ // Send to monitoring service
1143
+ sendToMonitoring({
1144
+ timestamp: source.timestamp,
1145
+ path,
1146
+ errorType: source.type,
1147
+ hookId: source.hookId,
1148
+ hookTag: source.hookTag,
1149
+ message: error.message,
1150
+ stack: source.stack
1151
+ });
1152
+ },
1153
+ { pattern: "**" }
1154
+ );
1155
+
1156
+ // Later: Analyze error patterns
1157
+ console.log("Error Statistics:", errorStats);
1158
+ // {
1159
+ // function: 5,
1160
+ // before: 2,
1161
+ // after: 1,
1162
+ // always: 0,
1163
+ // byHook: {
1164
+ // "validate-input": 2,
1165
+ // "format-output": 1
1166
+ // }
1167
+ // }
1168
+ ```
1169
+
1170
+ **Important Notes:**
1171
+
1172
+ - Errors from `before` and `after` hooks are re-thrown after error hooks execute
1173
+ - Errors from `always` hooks are caught and logged but do NOT crash execution
1174
+ - Error hooks themselves do not receive errors from other error hooks (no recursion)
1175
+ - The `_hookSourceReported` flag prevents double-reporting of errors
1176
+
1177
+ #### Cross-Mode Compatibility
1178
+
1179
+ Hooks work identically across all configurations:
1180
+
1181
+ ```javascript
1182
+ // Eager + AsyncLocalStorage
1183
+ const api1 = await slothlet({ dir: "./api", lazy: false, runtime: "async", hooks: true });
1184
+
1185
+ // Eager + Live Bindings
1186
+ const api2 = await slothlet({ dir: "./api", lazy: false, runtime: "live", hooks: true });
1187
+
1188
+ // Lazy + AsyncLocalStorage
1189
+ const api3 = await slothlet({ dir: "./api", lazy: true, runtime: "async", hooks: true });
1190
+
1191
+ // Lazy + Live Bindings
1192
+ const api4 = await slothlet({ dir: "./api", lazy: true, runtime: "live", hooks: true });
1193
+
1194
+ // Same hook code works with all configurations
1195
+ [api1, api2, api3, api4].forEach((api) => {
1196
+ api.hooks.on(
1197
+ "universal",
1198
+ "before",
1199
+ ({ args }) => {
1200
+ return [args[0] * 10, args[1] * 10];
1201
+ },
1202
+ { pattern: "math.add" }
1203
+ );
1204
+ });
1205
+ ```
1206
+
1207
+ **Key Benefits:**
1208
+
1209
+ - ✅ **Universal**: Works across all 4 mode/runtime combinations
1210
+ - ✅ **Flexible**: Pattern matching with wildcards and priorities
1211
+ - ✅ **Powerful**: Modify args, transform results, observe execution
1212
+ - ✅ **Composable**: Chain multiple hooks with priority control
1213
+ - ✅ **Dynamic**: Enable/disable at runtime globally or by pattern
1214
+ - ✅ **Observable**: Separate hook types for different responsibilities
1215
+
715
1216
  ### API Mode Configuration
716
1217
 
717
1218
  The `api_mode` option controls how slothlet handles root-level default function exports:
@@ -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
  }