@backendkit-labs/auto-learning 0.1.0 → 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;
@@ -41,6 +51,7 @@ __export(src_exports, {
41
51
  DEFAULT_LOOP_CONFIG: () => DEFAULT_LOOP_CONFIG,
42
52
  DEFAULT_TUNER_CONFIG: () => DEFAULT_TUNER_CONFIG,
43
53
  FeedbackLoop: () => FeedbackLoop,
54
+ FileStorageAdapter: () => FileStorageAdapter,
44
55
  InMemoryStorage: () => InMemoryStorage,
45
56
  NoopObservabilityAdapter: () => NoopObservabilityAdapter,
46
57
  PatternRegistry: () => PatternRegistry
@@ -248,11 +259,9 @@ var DEFAULT_TUNER_CONFIG = {
248
259
  // src/core/config-tuner/config-tuner.ts
249
260
  var import_result3 = require("@backendkit-labs/result");
250
261
  var DEFAULT_CONFIG = {
251
- timeoutMs: 1e4,
252
- maxRetries: 3,
253
- circuitBreakerThreshold: 0.5,
254
- circuitBreakerHalfOpenAfterMs: 3e4,
255
- bulkheadMaxConcurrent: 10
262
+ circuitBreaker: { failureThreshold: 50, openTimeoutMs: 3e4 },
263
+ bulkhead: { maxConcurrentCalls: 10 },
264
+ httpClient: { timeoutMs: 1e4, maxRetries: 3 }
256
265
  };
257
266
  var ConfigTuner = class {
258
267
  constructor(storage, observability, tunerConfig) {
@@ -269,51 +278,56 @@ var ConfigTuner = class {
269
278
  listeners = [];
270
279
  lastChangeAt = 0;
271
280
  getCurrentConfig() {
272
- return { ...this.currentConfig };
281
+ return {
282
+ circuitBreaker: { ...this.currentConfig.circuitBreaker },
283
+ bulkhead: { ...this.currentConfig.bulkhead },
284
+ httpClient: { ...this.currentConfig.httpClient }
285
+ };
273
286
  }
274
287
  tune(aggregates, anomalies) {
275
288
  if (aggregates.length === 0) {
276
289
  return (0, import_result3.ok)(this.getCurrentConfig());
277
290
  }
278
- const newConfig = { ...this.currentConfig };
279
- 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();
280
297
  const maxP95 = Math.max(...aggregates.map((a) => a.p95Ms));
281
298
  const targetTimeout = Math.min(
282
299
  Math.max(maxP95 * 2, this.config.minTimeoutMs),
283
300
  this.config.maxTimeoutMs
284
301
  );
285
- if (Math.abs(targetTimeout - newConfig.timeoutMs) > this.config.adjustmentStepMs) {
286
- newConfig.timeoutMs = this.smoothValue(
287
- newConfig.timeoutMs,
288
- targetTimeout
289
- );
290
- 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");
291
305
  }
292
306
  const avgErrorRate = aggregates.reduce((sum, a) => sum + a.errorRate, 0) / aggregates.length;
293
307
  if (avgErrorRate > 0.1) {
294
- newConfig.maxRetries = Math.min(newConfig.maxRetries + 1, 5);
295
- changes.maxRetries = newConfig.maxRetries;
296
- } else if (avgErrorRate < 0.01 && newConfig.maxRetries > 1) {
297
- newConfig.maxRetries = Math.max(newConfig.maxRetries - 1, 0);
298
- 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");
299
313
  }
300
314
  const criticalAnomalies = anomalies.filter(
301
315
  (a) => a.severity === "critical" || a.severity === "high"
302
316
  ).length;
303
317
  if (criticalAnomalies > 0) {
304
- newConfig.circuitBreakerThreshold = Math.max(
305
- this.currentConfig.circuitBreakerThreshold - 0.1 * criticalAnomalies,
306
- 0.1
318
+ newConfig.circuitBreaker.failureThreshold = Math.max(
319
+ this.currentConfig.circuitBreaker.failureThreshold - 10 * criticalAnomalies,
320
+ 10
307
321
  );
308
- changes.circuitBreakerThreshold = newConfig.circuitBreakerThreshold;
322
+ changedSections.add("circuitBreaker");
309
323
  } else if (anomalies.length === 0) {
310
- newConfig.circuitBreakerThreshold = Math.min(
311
- this.currentConfig.circuitBreakerThreshold + 0.05,
312
- 0.8
324
+ newConfig.circuitBreaker.failureThreshold = Math.min(
325
+ this.currentConfig.circuitBreaker.failureThreshold + 5,
326
+ 80
313
327
  );
314
- changes.circuitBreakerThreshold = newConfig.circuitBreakerThreshold;
328
+ changedSections.add("circuitBreaker");
315
329
  }
316
- if (Object.keys(changes).length > 0) {
330
+ if (changedSections.size > 0) {
317
331
  const now = Date.now();
318
332
  if (now - this.lastChangeAt > 6e4) {
319
333
  this.currentConfig = newConfig;
@@ -322,6 +336,9 @@ var ConfigTuner = class {
322
336
  if (!saveResult.ok) {
323
337
  return (0, import_result3.fail)(storageError("Failed to save config", saveResult.error));
324
338
  }
339
+ const changes = Object.fromEntries(
340
+ [...changedSections].map((s) => [s, newConfig[s]])
341
+ );
325
342
  this.observability.info("Config tuned", { changes });
326
343
  this.observability.incrementMetric("config.changes", 1);
327
344
  for (const listener of this.listeners) {
@@ -332,7 +349,11 @@ var ConfigTuner = class {
332
349
  return (0, import_result3.ok)(this.getCurrentConfig());
333
350
  }
334
351
  reset() {
335
- this.currentConfig = { ...DEFAULT_CONFIG };
352
+ this.currentConfig = {
353
+ circuitBreaker: { ...DEFAULT_CONFIG.circuitBreaker },
354
+ bulkhead: { ...DEFAULT_CONFIG.bulkhead },
355
+ httpClient: { ...DEFAULT_CONFIG.httpClient }
356
+ };
336
357
  const saveResult = this.storage.saveConfig(this.currentConfig);
337
358
  if (!saveResult.ok) {
338
359
  return (0, import_result3.fail)(storageError("Failed to reset config", saveResult.error));
@@ -464,9 +485,8 @@ var FeedbackLoop = class {
464
485
  const newConfig = tuneResult.value;
465
486
  const previousConfig = this.configTuner.getCurrentConfig();
466
487
  const configChanges = {};
467
- const configKeys = Object.keys(newConfig);
468
- for (const key of configKeys) {
469
- if (newConfig[key] !== previousConfig[key]) {
488
+ for (const key of Object.keys(newConfig)) {
489
+ if (JSON.stringify(newConfig[key]) !== JSON.stringify(previousConfig[key])) {
470
490
  configChanges[key] = newConfig[key];
471
491
  }
472
492
  }
@@ -504,11 +524,9 @@ var FeedbackLoop = class {
504
524
  // src/core/persistence/in-memory-storage.ts
505
525
  var import_result5 = require("@backendkit-labs/result");
506
526
  var DEFAULT_CONFIG2 = {
507
- timeoutMs: 1e4,
508
- maxRetries: 3,
509
- circuitBreakerThreshold: 0.5,
510
- circuitBreakerHalfOpenAfterMs: 3e4,
511
- bulkheadMaxConcurrent: 10
527
+ circuitBreaker: { failureThreshold: 50, openTimeoutMs: 3e4 },
528
+ bulkhead: { maxConcurrentCalls: 10 },
529
+ httpClient: { timeoutMs: 1e4, maxRetries: 3 }
512
530
  };
513
531
  function percentile(sorted, p) {
514
532
  if (sorted.length === 0) return 0;
@@ -590,7 +608,11 @@ var InMemoryStorage = class {
590
608
  }
591
609
  saveConfig(config) {
592
610
  try {
593
- this.config = { ...config };
611
+ this.config = {
612
+ circuitBreaker: { ...config.circuitBreaker },
613
+ bulkhead: { ...config.bulkhead },
614
+ httpClient: { ...config.httpClient }
615
+ };
594
616
  return (0, import_result5.ok)(void 0);
595
617
  } catch (e) {
596
618
  return (0, import_result5.fail)(storageError("Failed to save config", e));
@@ -598,7 +620,11 @@ var InMemoryStorage = class {
598
620
  }
599
621
  loadConfig() {
600
622
  try {
601
- 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
+ });
602
628
  } catch (e) {
603
629
  return (0, import_result5.fail)(storageError("Failed to load config", e));
604
630
  }
@@ -632,6 +658,40 @@ var InMemoryStorage = class {
632
658
  }
633
659
  };
634
660
 
661
+ // src/core/persistence/file-storage.ts
662
+ var import_fs = require("fs");
663
+ var import_path = require("path");
664
+ var import_result6 = require("@backendkit-labs/result");
665
+ var FileStorageAdapter = class extends InMemoryStorage {
666
+ constructor(filePath = "./auto-learning-config.json") {
667
+ super();
668
+ this.filePath = filePath;
669
+ }
670
+ filePath;
671
+ saveConfig(config) {
672
+ try {
673
+ (0, import_fs.mkdirSync)((0, import_path.dirname)(this.filePath), { recursive: true });
674
+ (0, import_fs.writeFileSync)(this.filePath, JSON.stringify(config, null, 2), "utf8");
675
+ return super.saveConfig(config);
676
+ } catch (e) {
677
+ return (0, import_result6.fail)(storageError("Failed to persist config to file", e));
678
+ }
679
+ }
680
+ loadConfig() {
681
+ try {
682
+ const raw = (0, import_fs.readFileSync)(this.filePath, "utf8");
683
+ const config = JSON.parse(raw);
684
+ super.saveConfig(config);
685
+ return (0, import_result6.ok)(config);
686
+ } catch (e) {
687
+ if (e instanceof Error && "code" in e && e.code === "ENOENT") {
688
+ return (0, import_result6.ok)(null);
689
+ }
690
+ return (0, import_result6.fail)(storageError("Failed to load config from file", e));
691
+ }
692
+ }
693
+ };
694
+
635
695
  // src/core/observability/noop-observability-adapter.ts
636
696
  var NoopObservabilityAdapter = class {
637
697
  info(_msg, _meta) {
@@ -702,7 +762,7 @@ var AutoLearningCore = class _AutoLearningCore {
702
762
  };
703
763
 
704
764
  // src/nestjs/auto-learning.module.ts
705
- var import_common2 = require("@nestjs/common");
765
+ var import_common3 = require("@nestjs/common");
706
766
  var import_core = require("@nestjs/core");
707
767
 
708
768
  // src/nestjs/auto-learning.interceptor.ts
@@ -759,6 +819,83 @@ AutoLearningInterceptor = __decorateClass([
759
819
  __decorateParam(1, (0, import_common.Inject)(AUTO_LEARNING_INSTANCE))
760
820
  ], AutoLearningInterceptor);
761
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
+
762
899
  // src/nestjs/backend-kit-observability-adapter.ts
763
900
  var BackendKitObservabilityAdapter = class {
764
901
  constructor(logger, metrics) {
@@ -819,7 +956,8 @@ var AutoLearningModule = class {
819
956
  {
820
957
  provide: import_core.APP_INTERCEPTOR,
821
958
  useClass: AutoLearningInterceptor
822
- }
959
+ },
960
+ AutoLearningAdaptersService
823
961
  ];
824
962
  return {
825
963
  module: AutoLearningModule,
@@ -830,12 +968,12 @@ var AutoLearningModule = class {
830
968
  }
831
969
  };
832
970
  AutoLearningModule = __decorateClass([
833
- (0, import_common2.Module)({})
971
+ (0, import_common3.Module)({})
834
972
  ], AutoLearningModule);
835
973
 
836
974
  // src/nestjs/auto-learning.decorator.ts
837
- var import_common3 = require("@nestjs/common");
838
- 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 ?? {});
839
977
  // Annotate the CommonJS export names for ESM import in node:
840
978
  0 && (module.exports = {
841
979
  AUTO_LEARNING_INSTANCE,
@@ -850,6 +988,7 @@ var AutoLearn = (options) => (0, import_common3.SetMetadata)(AUTO_LEARN_METADATA
850
988
  DEFAULT_LOOP_CONFIG,
851
989
  DEFAULT_TUNER_CONFIG,
852
990
  FeedbackLoop,
991
+ FileStorageAdapter,
853
992
  InMemoryStorage,
854
993
  NoopObservabilityAdapter,
855
994
  PatternRegistry