@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/AGENT-USAGE.md +536 -0
- package/API-RULES-CONDITIONS.md +367 -0
- package/API-RULES.md +777 -0
- package/README.md +502 -1
- package/dist/lib/helpers/als-eventemitter.mjs +137 -0
- package/dist/lib/helpers/hooks.mjs +389 -0
- package/dist/lib/modes/slothlet_lazy.mjs +25 -19
- package/dist/lib/runtime/runtime-asynclocalstorage.mjs +97 -16
- package/dist/lib/runtime/runtime-livebindings.mjs +127 -5
- package/dist/slothlet.mjs +69 -2
- package/package.json +6 -3
- package/types/dist/lib/helpers/als-eventemitter.d.mts +33 -0
- package/types/dist/lib/helpers/als-eventemitter.d.mts.map +1 -1
- package/types/dist/lib/helpers/hooks.d.mts +342 -0
- package/types/dist/lib/helpers/hooks.d.mts.map +1 -0
- package/types/dist/lib/runtime/runtime-asynclocalstorage.d.mts.map +1 -1
- package/types/dist/lib/runtime/runtime-livebindings.d.mts +4 -3
- package/types/dist/lib/runtime/runtime-livebindings.d.mts.map +1 -1
- package/types/dist/slothlet.d.mts.map +1 -1
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
|
|
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
|
}
|