@cldmv/slothlet 2.6.3 → 2.7.0

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