@backendkit-labs/auto-learning 0.1.1 → 0.1.2

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.
@@ -36,11 +36,17 @@ type AnomalyReport$1 = {
36
36
  detectedAt: Date;
37
37
  };
38
38
  type TunableConfig = {
39
- timeoutMs: number;
40
- maxRetries: number;
41
- circuitBreakerThreshold: number;
42
- circuitBreakerHalfOpenAfterMs: number;
43
- bulkheadMaxConcurrent: number;
39
+ circuitBreaker: {
40
+ failureThreshold: number;
41
+ openTimeoutMs: number;
42
+ };
43
+ bulkhead: {
44
+ maxConcurrentCalls: number;
45
+ };
46
+ httpClient: {
47
+ timeoutMs: number;
48
+ maxRetries: number;
49
+ };
44
50
  };
45
51
  type LearningCycleEvent = {
46
52
  cycleId: string;
@@ -189,6 +195,10 @@ type AutoLearningModuleOptions = {
189
195
  };
190
196
  };
191
197
  coreOptions?: Omit<AutoLearningCoreOptions, 'storage' | 'observability'>;
198
+ adapters?: {
199
+ circuitBreaker?: boolean;
200
+ bulkhead?: boolean;
201
+ };
192
202
  };
193
203
  declare class AutoLearningModule {
194
204
  static forRoot(options?: AutoLearningModuleOptions): DynamicModule;
@@ -36,11 +36,17 @@ type AnomalyReport$1 = {
36
36
  detectedAt: Date;
37
37
  };
38
38
  type TunableConfig = {
39
- timeoutMs: number;
40
- maxRetries: number;
41
- circuitBreakerThreshold: number;
42
- circuitBreakerHalfOpenAfterMs: number;
43
- bulkheadMaxConcurrent: number;
39
+ circuitBreaker: {
40
+ failureThreshold: number;
41
+ openTimeoutMs: number;
42
+ };
43
+ bulkhead: {
44
+ maxConcurrentCalls: number;
45
+ };
46
+ httpClient: {
47
+ timeoutMs: number;
48
+ maxRetries: number;
49
+ };
44
50
  };
45
51
  type LearningCycleEvent = {
46
52
  cycleId: string;
@@ -189,6 +195,10 @@ type AutoLearningModuleOptions = {
189
195
  };
190
196
  };
191
197
  coreOptions?: Omit<AutoLearningCoreOptions, 'storage' | 'observability'>;
198
+ adapters?: {
199
+ circuitBreaker?: boolean;
200
+ bulkhead?: boolean;
201
+ };
192
202
  };
193
203
  declare class AutoLearningModule {
194
204
  static forRoot(options?: AutoLearningModuleOptions): DynamicModule;
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
  var __decorateClass = (decorators, target, key, kind) => {
20
30
  var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
@@ -249,11 +259,9 @@ var DEFAULT_TUNER_CONFIG = {
249
259
  // src/core/config-tuner/config-tuner.ts
250
260
  var import_result3 = require("@backendkit-labs/result");
251
261
  var DEFAULT_CONFIG = {
252
- timeoutMs: 1e4,
253
- maxRetries: 3,
254
- circuitBreakerThreshold: 0.5,
255
- circuitBreakerHalfOpenAfterMs: 3e4,
256
- bulkheadMaxConcurrent: 10
262
+ circuitBreaker: { failureThreshold: 50, openTimeoutMs: 3e4 },
263
+ bulkhead: { maxConcurrentCalls: 10 },
264
+ httpClient: { timeoutMs: 1e4, maxRetries: 3 }
257
265
  };
258
266
  var ConfigTuner = class {
259
267
  constructor(storage, observability, tunerConfig) {
@@ -270,51 +278,56 @@ var ConfigTuner = class {
270
278
  listeners = [];
271
279
  lastChangeAt = 0;
272
280
  getCurrentConfig() {
273
- return { ...this.currentConfig };
281
+ return {
282
+ circuitBreaker: { ...this.currentConfig.circuitBreaker },
283
+ bulkhead: { ...this.currentConfig.bulkhead },
284
+ httpClient: { ...this.currentConfig.httpClient }
285
+ };
274
286
  }
275
287
  tune(aggregates, anomalies) {
276
288
  if (aggregates.length === 0) {
277
289
  return (0, import_result3.ok)(this.getCurrentConfig());
278
290
  }
279
- const newConfig = { ...this.currentConfig };
280
- const changes = {};
291
+ const newConfig = {
292
+ circuitBreaker: { ...this.currentConfig.circuitBreaker },
293
+ bulkhead: { ...this.currentConfig.bulkhead },
294
+ httpClient: { ...this.currentConfig.httpClient }
295
+ };
296
+ const changedSections = /* @__PURE__ */ new Set();
281
297
  const maxP95 = Math.max(...aggregates.map((a) => a.p95Ms));
282
298
  const targetTimeout = Math.min(
283
299
  Math.max(maxP95 * 2, this.config.minTimeoutMs),
284
300
  this.config.maxTimeoutMs
285
301
  );
286
- if (Math.abs(targetTimeout - newConfig.timeoutMs) > this.config.adjustmentStepMs) {
287
- newConfig.timeoutMs = this.smoothValue(
288
- newConfig.timeoutMs,
289
- targetTimeout
290
- );
291
- changes.timeoutMs = newConfig.timeoutMs;
302
+ if (Math.abs(targetTimeout - newConfig.httpClient.timeoutMs) > this.config.adjustmentStepMs) {
303
+ newConfig.httpClient.timeoutMs = this.smoothValue(newConfig.httpClient.timeoutMs, targetTimeout);
304
+ changedSections.add("httpClient");
292
305
  }
293
306
  const avgErrorRate = aggregates.reduce((sum, a) => sum + a.errorRate, 0) / aggregates.length;
294
307
  if (avgErrorRate > 0.1) {
295
- newConfig.maxRetries = Math.min(newConfig.maxRetries + 1, 5);
296
- changes.maxRetries = newConfig.maxRetries;
297
- } else if (avgErrorRate < 0.01 && newConfig.maxRetries > 1) {
298
- newConfig.maxRetries = Math.max(newConfig.maxRetries - 1, 0);
299
- changes.maxRetries = newConfig.maxRetries;
308
+ newConfig.httpClient.maxRetries = Math.min(newConfig.httpClient.maxRetries + 1, 5);
309
+ changedSections.add("httpClient");
310
+ } else if (avgErrorRate < 0.01 && newConfig.httpClient.maxRetries > 1) {
311
+ newConfig.httpClient.maxRetries = Math.max(newConfig.httpClient.maxRetries - 1, 0);
312
+ changedSections.add("httpClient");
300
313
  }
301
314
  const criticalAnomalies = anomalies.filter(
302
315
  (a) => a.severity === "critical" || a.severity === "high"
303
316
  ).length;
304
317
  if (criticalAnomalies > 0) {
305
- newConfig.circuitBreakerThreshold = Math.max(
306
- this.currentConfig.circuitBreakerThreshold - 0.1 * criticalAnomalies,
307
- 0.1
318
+ newConfig.circuitBreaker.failureThreshold = Math.max(
319
+ this.currentConfig.circuitBreaker.failureThreshold - 10 * criticalAnomalies,
320
+ 10
308
321
  );
309
- changes.circuitBreakerThreshold = newConfig.circuitBreakerThreshold;
322
+ changedSections.add("circuitBreaker");
310
323
  } else if (anomalies.length === 0) {
311
- newConfig.circuitBreakerThreshold = Math.min(
312
- this.currentConfig.circuitBreakerThreshold + 0.05,
313
- 0.8
324
+ newConfig.circuitBreaker.failureThreshold = Math.min(
325
+ this.currentConfig.circuitBreaker.failureThreshold + 5,
326
+ 80
314
327
  );
315
- changes.circuitBreakerThreshold = newConfig.circuitBreakerThreshold;
328
+ changedSections.add("circuitBreaker");
316
329
  }
317
- if (Object.keys(changes).length > 0) {
330
+ if (changedSections.size > 0) {
318
331
  const now = Date.now();
319
332
  if (now - this.lastChangeAt > 6e4) {
320
333
  this.currentConfig = newConfig;
@@ -323,6 +336,9 @@ var ConfigTuner = class {
323
336
  if (!saveResult.ok) {
324
337
  return (0, import_result3.fail)(storageError("Failed to save config", saveResult.error));
325
338
  }
339
+ const changes = Object.fromEntries(
340
+ [...changedSections].map((s) => [s, newConfig[s]])
341
+ );
326
342
  this.observability.info("Config tuned", { changes });
327
343
  this.observability.incrementMetric("config.changes", 1);
328
344
  for (const listener of this.listeners) {
@@ -333,7 +349,11 @@ var ConfigTuner = class {
333
349
  return (0, import_result3.ok)(this.getCurrentConfig());
334
350
  }
335
351
  reset() {
336
- this.currentConfig = { ...DEFAULT_CONFIG };
352
+ this.currentConfig = {
353
+ circuitBreaker: { ...DEFAULT_CONFIG.circuitBreaker },
354
+ bulkhead: { ...DEFAULT_CONFIG.bulkhead },
355
+ httpClient: { ...DEFAULT_CONFIG.httpClient }
356
+ };
337
357
  const saveResult = this.storage.saveConfig(this.currentConfig);
338
358
  if (!saveResult.ok) {
339
359
  return (0, import_result3.fail)(storageError("Failed to reset config", saveResult.error));
@@ -465,9 +485,8 @@ var FeedbackLoop = class {
465
485
  const newConfig = tuneResult.value;
466
486
  const previousConfig = this.configTuner.getCurrentConfig();
467
487
  const configChanges = {};
468
- const configKeys = Object.keys(newConfig);
469
- for (const key of configKeys) {
470
- if (newConfig[key] !== previousConfig[key]) {
488
+ for (const key of Object.keys(newConfig)) {
489
+ if (JSON.stringify(newConfig[key]) !== JSON.stringify(previousConfig[key])) {
471
490
  configChanges[key] = newConfig[key];
472
491
  }
473
492
  }
@@ -505,11 +524,9 @@ var FeedbackLoop = class {
505
524
  // src/core/persistence/in-memory-storage.ts
506
525
  var import_result5 = require("@backendkit-labs/result");
507
526
  var DEFAULT_CONFIG2 = {
508
- timeoutMs: 1e4,
509
- maxRetries: 3,
510
- circuitBreakerThreshold: 0.5,
511
- circuitBreakerHalfOpenAfterMs: 3e4,
512
- bulkheadMaxConcurrent: 10
527
+ circuitBreaker: { failureThreshold: 50, openTimeoutMs: 3e4 },
528
+ bulkhead: { maxConcurrentCalls: 10 },
529
+ httpClient: { timeoutMs: 1e4, maxRetries: 3 }
513
530
  };
514
531
  function percentile(sorted, p) {
515
532
  if (sorted.length === 0) return 0;
@@ -591,7 +608,11 @@ var InMemoryStorage = class {
591
608
  }
592
609
  saveConfig(config) {
593
610
  try {
594
- this.config = { ...config };
611
+ this.config = {
612
+ circuitBreaker: { ...config.circuitBreaker },
613
+ bulkhead: { ...config.bulkhead },
614
+ httpClient: { ...config.httpClient }
615
+ };
595
616
  return (0, import_result5.ok)(void 0);
596
617
  } catch (e) {
597
618
  return (0, import_result5.fail)(storageError("Failed to save config", e));
@@ -599,7 +620,11 @@ var InMemoryStorage = class {
599
620
  }
600
621
  loadConfig() {
601
622
  try {
602
- return (0, import_result5.ok)(this.config);
623
+ return (0, import_result5.ok)({
624
+ circuitBreaker: { ...this.config.circuitBreaker },
625
+ bulkhead: { ...this.config.bulkhead },
626
+ httpClient: { ...this.config.httpClient }
627
+ });
603
628
  } catch (e) {
604
629
  return (0, import_result5.fail)(storageError("Failed to load config", e));
605
630
  }
@@ -737,7 +762,7 @@ var AutoLearningCore = class _AutoLearningCore {
737
762
  };
738
763
 
739
764
  // src/nestjs/auto-learning.module.ts
740
- var import_common2 = require("@nestjs/common");
765
+ var import_common3 = require("@nestjs/common");
741
766
  var import_core = require("@nestjs/core");
742
767
 
743
768
  // src/nestjs/auto-learning.interceptor.ts
@@ -794,6 +819,83 @@ AutoLearningInterceptor = __decorateClass([
794
819
  __decorateParam(1, (0, import_common.Inject)(AUTO_LEARNING_INSTANCE))
795
820
  ], AutoLearningInterceptor);
796
821
 
822
+ // src/nestjs/auto-learning-adapters.service.ts
823
+ var import_common2 = require("@nestjs/common");
824
+ var AutoLearningAdaptersService = class {
825
+ constructor(core, options, moduleRef) {
826
+ this.core = core;
827
+ this.options = options;
828
+ this.moduleRef = moduleRef;
829
+ }
830
+ core;
831
+ options;
832
+ moduleRef;
833
+ cbRegistry = null;
834
+ bhRegistry = null;
835
+ async onModuleInit() {
836
+ await this.resolveRegistries();
837
+ if (this.cbRegistry || this.bhRegistry) {
838
+ this.core.onConfigChange((config) => this.applyConfig(config));
839
+ }
840
+ }
841
+ async resolveRegistries() {
842
+ if (this.options.adapters?.circuitBreaker) {
843
+ try {
844
+ const mod = await import("@backendkit-labs/circuit-breaker");
845
+ this.cbRegistry = this.moduleRef.get(mod.CircuitBreakerRegistry, { strict: false });
846
+ this.core.observability.info("CircuitBreakerRegistry adapter connected");
847
+ } catch {
848
+ this.core.observability.warn(
849
+ "adapters.circuitBreaker=true but CircuitBreakerModule is not imported \u2014 adapter skipped"
850
+ );
851
+ }
852
+ }
853
+ if (this.options.adapters?.bulkhead) {
854
+ try {
855
+ const mod = await import("@backendkit-labs/bulkhead");
856
+ this.bhRegistry = this.moduleRef.get(mod.BulkheadRegistry, { strict: false });
857
+ this.core.observability.info("BulkheadRegistry adapter connected");
858
+ } catch {
859
+ this.core.observability.warn(
860
+ "adapters.bulkhead=true but BulkheadModule is not imported \u2014 adapter skipped"
861
+ );
862
+ }
863
+ }
864
+ }
865
+ applyConfig(config) {
866
+ if (this.cbRegistry) {
867
+ const allMetrics = this.cbRegistry.getAllMetrics();
868
+ for (const name of Object.keys(allMetrics)) {
869
+ const cb = this.cbRegistry.getOrCreate({ name });
870
+ cb.updateConfig({
871
+ failureThreshold: config.circuitBreaker.failureThreshold,
872
+ openTimeoutMs: config.circuitBreaker.openTimeoutMs
873
+ });
874
+ }
875
+ this.core.observability.debug("Circuit breaker config updated", {
876
+ ...config.circuitBreaker,
877
+ affected: Object.keys(allMetrics).length
878
+ });
879
+ }
880
+ if (this.bhRegistry) {
881
+ const allMetrics = this.bhRegistry.getAllMetrics();
882
+ for (const name of Object.keys(allMetrics)) {
883
+ const bh = this.bhRegistry.getOrCreate({ name });
884
+ bh.updateConfig({ maxConcurrentCalls: config.bulkhead.maxConcurrentCalls });
885
+ }
886
+ this.core.observability.debug("Bulkhead config updated", {
887
+ ...config.bulkhead,
888
+ affected: Object.keys(allMetrics).length
889
+ });
890
+ }
891
+ }
892
+ };
893
+ AutoLearningAdaptersService = __decorateClass([
894
+ (0, import_common2.Injectable)(),
895
+ __decorateParam(0, (0, import_common2.Inject)(AUTO_LEARNING_INSTANCE)),
896
+ __decorateParam(1, (0, import_common2.Inject)(AUTO_LEARNING_OPTIONS))
897
+ ], AutoLearningAdaptersService);
898
+
797
899
  // src/nestjs/backend-kit-observability-adapter.ts
798
900
  var BackendKitObservabilityAdapter = class {
799
901
  constructor(logger, metrics) {
@@ -854,7 +956,8 @@ var AutoLearningModule = class {
854
956
  {
855
957
  provide: import_core.APP_INTERCEPTOR,
856
958
  useClass: AutoLearningInterceptor
857
- }
959
+ },
960
+ AutoLearningAdaptersService
858
961
  ];
859
962
  return {
860
963
  module: AutoLearningModule,
@@ -865,12 +968,12 @@ var AutoLearningModule = class {
865
968
  }
866
969
  };
867
970
  AutoLearningModule = __decorateClass([
868
- (0, import_common2.Module)({})
971
+ (0, import_common3.Module)({})
869
972
  ], AutoLearningModule);
870
973
 
871
974
  // src/nestjs/auto-learning.decorator.ts
872
- var import_common3 = require("@nestjs/common");
873
- var AutoLearn = (options) => (0, import_common3.SetMetadata)(AUTO_LEARN_METADATA, options ?? {});
975
+ var import_common4 = require("@nestjs/common");
976
+ var AutoLearn = (options) => (0, import_common4.SetMetadata)(AUTO_LEARN_METADATA, options ?? {});
874
977
  // Annotate the CommonJS export names for ESM import in node:
875
978
  0 && (module.exports = {
876
979
  AUTO_LEARNING_INSTANCE,