@skillsmith/core 0.1.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/dist/.tsbuildinfo +1 -0
- package/dist/src/__tests__/errors.test.d.ts +5 -0
- package/dist/src/__tests__/errors.test.d.ts.map +1 -0
- package/dist/src/__tests__/errors.test.js +115 -0
- package/dist/src/__tests__/errors.test.js.map +1 -0
- package/dist/src/activation/ActivationManager.d.ts +141 -0
- package/dist/src/activation/ActivationManager.d.ts.map +1 -0
- package/dist/src/activation/ActivationManager.js +282 -0
- package/dist/src/activation/ActivationManager.js.map +1 -0
- package/dist/src/activation/ZeroConfigActivator.d.ts +126 -0
- package/dist/src/activation/ZeroConfigActivator.d.ts.map +1 -0
- package/dist/src/activation/ZeroConfigActivator.js +259 -0
- package/dist/src/activation/ZeroConfigActivator.js.map +1 -0
- package/dist/src/activation/index.d.ts +8 -0
- package/dist/src/activation/index.d.ts.map +1 -0
- package/dist/src/activation/index.js +8 -0
- package/dist/src/activation/index.js.map +1 -0
- package/dist/src/analysis/CodebaseAnalyzer.d.ts +175 -0
- package/dist/src/analysis/CodebaseAnalyzer.d.ts.map +1 -0
- package/dist/src/analysis/CodebaseAnalyzer.js +495 -0
- package/dist/src/analysis/CodebaseAnalyzer.js.map +1 -0
- package/dist/src/analysis/index.d.ts +10 -0
- package/dist/src/analysis/index.d.ts.map +1 -0
- package/dist/src/analysis/index.js +10 -0
- package/dist/src/analysis/index.js.map +1 -0
- package/dist/src/analytics/AnalyticsRepository.d.ts +97 -0
- package/dist/src/analytics/AnalyticsRepository.d.ts.map +1 -0
- package/dist/src/analytics/AnalyticsRepository.js +376 -0
- package/dist/src/analytics/AnalyticsRepository.js.map +1 -0
- package/dist/src/analytics/ExperimentService.d.ts +70 -0
- package/dist/src/analytics/ExperimentService.d.ts.map +1 -0
- package/dist/src/analytics/ExperimentService.js +251 -0
- package/dist/src/analytics/ExperimentService.js.map +1 -0
- package/dist/src/analytics/ROIDashboardService.d.ts +55 -0
- package/dist/src/analytics/ROIDashboardService.d.ts.map +1 -0
- package/dist/src/analytics/ROIDashboardService.js +304 -0
- package/dist/src/analytics/ROIDashboardService.js.map +1 -0
- package/dist/src/analytics/UsageAnalyticsService.d.ts +55 -0
- package/dist/src/analytics/UsageAnalyticsService.d.ts.map +1 -0
- package/dist/src/analytics/UsageAnalyticsService.js +180 -0
- package/dist/src/analytics/UsageAnalyticsService.js.map +1 -0
- package/dist/src/analytics/anonymizer.d.ts +53 -0
- package/dist/src/analytics/anonymizer.d.ts.map +1 -0
- package/dist/src/analytics/anonymizer.js +107 -0
- package/dist/src/analytics/anonymizer.js.map +1 -0
- package/dist/src/analytics/constants.d.ts +19 -0
- package/dist/src/analytics/constants.d.ts.map +1 -0
- package/dist/src/analytics/constants.js +19 -0
- package/dist/src/analytics/constants.js.map +1 -0
- package/dist/src/analytics/index.d.ts +23 -0
- package/dist/src/analytics/index.d.ts.map +1 -0
- package/dist/src/analytics/index.js +23 -0
- package/dist/src/analytics/index.js.map +1 -0
- package/dist/src/analytics/metrics-aggregator.d.ts +98 -0
- package/dist/src/analytics/metrics-aggregator.d.ts.map +1 -0
- package/dist/src/analytics/metrics-aggregator.js +176 -0
- package/dist/src/analytics/metrics-aggregator.js.map +1 -0
- package/dist/src/analytics/metrics-exporter.d.ts +148 -0
- package/dist/src/analytics/metrics-exporter.d.ts.map +1 -0
- package/dist/src/analytics/metrics-exporter.js +244 -0
- package/dist/src/analytics/metrics-exporter.js.map +1 -0
- package/dist/src/analytics/schema.d.ts +20 -0
- package/dist/src/analytics/schema.d.ts.map +1 -0
- package/dist/src/analytics/schema.js +125 -0
- package/dist/src/analytics/schema.js.map +1 -0
- package/dist/src/analytics/storage.d.ts +76 -0
- package/dist/src/analytics/storage.d.ts.map +1 -0
- package/dist/src/analytics/storage.js +180 -0
- package/dist/src/analytics/storage.js.map +1 -0
- package/dist/src/analytics/types.d.ts +277 -0
- package/dist/src/analytics/types.d.ts.map +1 -0
- package/dist/src/analytics/types.js +10 -0
- package/dist/src/analytics/types.js.map +1 -0
- package/dist/src/analytics/usage-tracker.d.ts +132 -0
- package/dist/src/analytics/usage-tracker.d.ts.map +1 -0
- package/dist/src/analytics/usage-tracker.js +213 -0
- package/dist/src/analytics/usage-tracker.js.map +1 -0
- package/dist/src/benchmarks/BenchmarkRunner.d.ts +258 -0
- package/dist/src/benchmarks/BenchmarkRunner.d.ts.map +1 -0
- package/dist/src/benchmarks/BenchmarkRunner.js +453 -0
- package/dist/src/benchmarks/BenchmarkRunner.js.map +1 -0
- package/dist/src/benchmarks/IndexBenchmark.d.ts +101 -0
- package/dist/src/benchmarks/IndexBenchmark.d.ts.map +1 -0
- package/dist/src/benchmarks/IndexBenchmark.js +314 -0
- package/dist/src/benchmarks/IndexBenchmark.js.map +1 -0
- package/dist/src/benchmarks/MemoryProfiler.d.ts +266 -0
- package/dist/src/benchmarks/MemoryProfiler.d.ts.map +1 -0
- package/dist/src/benchmarks/MemoryProfiler.js +404 -0
- package/dist/src/benchmarks/MemoryProfiler.js.map +1 -0
- package/dist/src/benchmarks/SearchBenchmark.d.ts +71 -0
- package/dist/src/benchmarks/SearchBenchmark.d.ts.map +1 -0
- package/dist/src/benchmarks/SearchBenchmark.js +321 -0
- package/dist/src/benchmarks/SearchBenchmark.js.map +1 -0
- package/dist/src/benchmarks/cacheBenchmark.d.ts +75 -0
- package/dist/src/benchmarks/cacheBenchmark.d.ts.map +1 -0
- package/dist/src/benchmarks/cacheBenchmark.js +325 -0
- package/dist/src/benchmarks/cacheBenchmark.js.map +1 -0
- package/dist/src/benchmarks/cli.d.ts +17 -0
- package/dist/src/benchmarks/cli.d.ts.map +1 -0
- package/dist/src/benchmarks/cli.js +140 -0
- package/dist/src/benchmarks/cli.js.map +1 -0
- package/dist/src/benchmarks/embeddingBenchmark.d.ts +86 -0
- package/dist/src/benchmarks/embeddingBenchmark.d.ts.map +1 -0
- package/dist/src/benchmarks/embeddingBenchmark.js +329 -0
- package/dist/src/benchmarks/embeddingBenchmark.js.map +1 -0
- package/dist/src/benchmarks/index.d.ts +51 -0
- package/dist/src/benchmarks/index.d.ts.map +1 -0
- package/dist/src/benchmarks/index.js +201 -0
- package/dist/src/benchmarks/index.js.map +1 -0
- package/dist/src/benchmarks/stats.d.ts +56 -0
- package/dist/src/benchmarks/stats.d.ts.map +1 -0
- package/dist/src/benchmarks/stats.js +86 -0
- package/dist/src/benchmarks/stats.js.map +1 -0
- package/dist/src/cache/CacheEntry.d.ts +101 -0
- package/dist/src/cache/CacheEntry.d.ts.map +1 -0
- package/dist/src/cache/CacheEntry.js +238 -0
- package/dist/src/cache/CacheEntry.js.map +1 -0
- package/dist/src/cache/CacheManager.d.ts +167 -0
- package/dist/src/cache/CacheManager.d.ts.map +1 -0
- package/dist/src/cache/CacheManager.js +346 -0
- package/dist/src/cache/CacheManager.js.map +1 -0
- package/dist/src/cache/TieredCache.d.ts +97 -0
- package/dist/src/cache/TieredCache.d.ts.map +1 -0
- package/dist/src/cache/TieredCache.js +352 -0
- package/dist/src/cache/TieredCache.js.map +1 -0
- package/dist/src/cache/index.d.ts +63 -0
- package/dist/src/cache/index.d.ts.map +1 -0
- package/dist/src/cache/index.js +91 -0
- package/dist/src/cache/index.js.map +1 -0
- package/dist/src/cache/lru.d.ts +68 -0
- package/dist/src/cache/lru.d.ts.map +1 -0
- package/dist/src/cache/lru.js +105 -0
- package/dist/src/cache/lru.js.map +1 -0
- package/dist/src/cache/sqlite.d.ts +59 -0
- package/dist/src/cache/sqlite.d.ts.map +1 -0
- package/dist/src/cache/sqlite.js +159 -0
- package/dist/src/cache/sqlite.js.map +1 -0
- package/dist/src/db/quarantine-schema.d.ts +81 -0
- package/dist/src/db/quarantine-schema.d.ts.map +1 -0
- package/dist/src/db/quarantine-schema.js +110 -0
- package/dist/src/db/quarantine-schema.js.map +1 -0
- package/dist/src/db/schema.d.ts +65 -0
- package/dist/src/db/schema.d.ts.map +1 -0
- package/dist/src/db/schema.js +318 -0
- package/dist/src/db/schema.js.map +1 -0
- package/dist/src/embeddings/index.d.ts +158 -0
- package/dist/src/embeddings/index.d.ts.map +1 -0
- package/dist/src/embeddings/index.js +397 -0
- package/dist/src/embeddings/index.js.map +1 -0
- package/dist/src/errors/SkillsmithError.d.ts +118 -0
- package/dist/src/errors/SkillsmithError.d.ts.map +1 -0
- package/dist/src/errors/SkillsmithError.js +194 -0
- package/dist/src/errors/SkillsmithError.js.map +1 -0
- package/dist/src/errors/index.d.ts +36 -0
- package/dist/src/errors/index.d.ts.map +1 -0
- package/dist/src/errors/index.js +36 -0
- package/dist/src/errors/index.js.map +1 -0
- package/dist/src/errors.d.ts +72 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +123 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +56 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +73 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/index.test.d.ts +2 -0
- package/dist/src/index.test.d.ts.map +1 -0
- package/dist/src/index.test.js +35 -0
- package/dist/src/index.test.js.map +1 -0
- package/dist/src/indexer/GitHubIndexer.d.ts +107 -0
- package/dist/src/indexer/GitHubIndexer.d.ts.map +1 -0
- package/dist/src/indexer/GitHubIndexer.js +202 -0
- package/dist/src/indexer/GitHubIndexer.js.map +1 -0
- package/dist/src/indexer/PartitionStrategy.d.ts +115 -0
- package/dist/src/indexer/PartitionStrategy.d.ts.map +1 -0
- package/dist/src/indexer/PartitionStrategy.js +207 -0
- package/dist/src/indexer/PartitionStrategy.js.map +1 -0
- package/dist/src/indexer/SkillParser.d.ts +112 -0
- package/dist/src/indexer/SkillParser.d.ts.map +1 -0
- package/dist/src/indexer/SkillParser.js +281 -0
- package/dist/src/indexer/SkillParser.js.map +1 -0
- package/dist/src/indexer/SwarmIndexer.d.ts +163 -0
- package/dist/src/indexer/SwarmIndexer.d.ts.map +1 -0
- package/dist/src/indexer/SwarmIndexer.js +300 -0
- package/dist/src/indexer/SwarmIndexer.js.map +1 -0
- package/dist/src/indexer/index.d.ts +12 -0
- package/dist/src/indexer/index.d.ts.map +1 -0
- package/dist/src/indexer/index.js +16 -0
- package/dist/src/indexer/index.js.map +1 -0
- package/dist/src/learning/interfaces.d.ts +338 -0
- package/dist/src/learning/interfaces.d.ts.map +1 -0
- package/dist/src/learning/interfaces.js +13 -0
- package/dist/src/learning/interfaces.js.map +1 -0
- package/dist/src/learning/types.d.ts +284 -0
- package/dist/src/learning/types.d.ts.map +1 -0
- package/dist/src/learning/types.js +112 -0
- package/dist/src/learning/types.js.map +1 -0
- package/dist/src/matching/OverlapDetector.d.ts +152 -0
- package/dist/src/matching/OverlapDetector.d.ts.map +1 -0
- package/dist/src/matching/OverlapDetector.js +218 -0
- package/dist/src/matching/OverlapDetector.js.map +1 -0
- package/dist/src/matching/SkillMatcher.d.ts +125 -0
- package/dist/src/matching/SkillMatcher.d.ts.map +1 -0
- package/dist/src/matching/SkillMatcher.js +206 -0
- package/dist/src/matching/SkillMatcher.js.map +1 -0
- package/dist/src/matching/index.d.ts +14 -0
- package/dist/src/matching/index.d.ts.map +1 -0
- package/dist/src/matching/index.js +12 -0
- package/dist/src/matching/index.js.map +1 -0
- package/dist/src/pipeline/DailyIndexPipeline.d.ts +220 -0
- package/dist/src/pipeline/DailyIndexPipeline.d.ts.map +1 -0
- package/dist/src/pipeline/DailyIndexPipeline.js +320 -0
- package/dist/src/pipeline/DailyIndexPipeline.js.map +1 -0
- package/dist/src/pipeline/index.d.ts +9 -0
- package/dist/src/pipeline/index.d.ts.map +1 -0
- package/dist/src/pipeline/index.js +9 -0
- package/dist/src/pipeline/index.js.map +1 -0
- package/dist/src/repositories/CacheRepository.d.ts +60 -0
- package/dist/src/repositories/CacheRepository.d.ts.map +1 -0
- package/dist/src/repositories/CacheRepository.js +148 -0
- package/dist/src/repositories/CacheRepository.js.map +1 -0
- package/dist/src/repositories/IndexerRepository.d.ts +126 -0
- package/dist/src/repositories/IndexerRepository.d.ts.map +1 -0
- package/dist/src/repositories/IndexerRepository.js +270 -0
- package/dist/src/repositories/IndexerRepository.js.map +1 -0
- package/dist/src/repositories/QuarantineRepository.d.ts +255 -0
- package/dist/src/repositories/QuarantineRepository.d.ts.map +1 -0
- package/dist/src/repositories/QuarantineRepository.js +445 -0
- package/dist/src/repositories/QuarantineRepository.js.map +1 -0
- package/dist/src/repositories/SkillRepository.d.ts +78 -0
- package/dist/src/repositories/SkillRepository.d.ts.map +1 -0
- package/dist/src/repositories/SkillRepository.js +208 -0
- package/dist/src/repositories/SkillRepository.js.map +1 -0
- package/dist/src/scoring/QualityScorer.d.ts +188 -0
- package/dist/src/scoring/QualityScorer.d.ts.map +1 -0
- package/dist/src/scoring/QualityScorer.js +342 -0
- package/dist/src/scoring/QualityScorer.js.map +1 -0
- package/dist/src/scoring/index.d.ts +9 -0
- package/dist/src/scoring/index.d.ts.map +1 -0
- package/dist/src/scoring/index.js +9 -0
- package/dist/src/scoring/index.js.map +1 -0
- package/dist/src/scripts/__tests__/scan-imported-skills.test.d.ts +5 -0
- package/dist/src/scripts/__tests__/scan-imported-skills.test.d.ts.map +1 -0
- package/dist/src/scripts/__tests__/scan-imported-skills.test.js +365 -0
- package/dist/src/scripts/__tests__/scan-imported-skills.test.js.map +1 -0
- package/dist/src/scripts/import-github-skills.d.ts +24 -0
- package/dist/src/scripts/import-github-skills.d.ts.map +1 -0
- package/dist/src/scripts/import-github-skills.js +545 -0
- package/dist/src/scripts/import-github-skills.js.map +1 -0
- package/dist/src/scripts/import-to-database.d.ts +60 -0
- package/dist/src/scripts/import-to-database.d.ts.map +1 -0
- package/dist/src/scripts/import-to-database.js +307 -0
- package/dist/src/scripts/import-to-database.js.map +1 -0
- package/dist/src/scripts/scan-imported-skills.d.ts +15 -0
- package/dist/src/scripts/scan-imported-skills.d.ts.map +1 -0
- package/dist/src/scripts/scan-imported-skills.js +405 -0
- package/dist/src/scripts/scan-imported-skills.js.map +1 -0
- package/dist/src/scripts/validate-skills.d.ts +180 -0
- package/dist/src/scripts/validate-skills.d.ts.map +1 -0
- package/dist/src/scripts/validate-skills.js +572 -0
- package/dist/src/scripts/validate-skills.js.map +1 -0
- package/dist/src/search/hybrid.d.ts +85 -0
- package/dist/src/search/hybrid.d.ts.map +1 -0
- package/dist/src/search/hybrid.js +291 -0
- package/dist/src/search/hybrid.js.map +1 -0
- package/dist/src/search/index.d.ts +6 -0
- package/dist/src/search/index.d.ts.map +1 -0
- package/dist/src/search/index.js +5 -0
- package/dist/src/search/index.js.map +1 -0
- package/dist/src/security/AuditLogger.d.ts +197 -0
- package/dist/src/security/AuditLogger.d.ts.map +1 -0
- package/dist/src/security/AuditLogger.js +398 -0
- package/dist/src/security/AuditLogger.js.map +1 -0
- package/dist/src/security/RateLimiter.d.ts +337 -0
- package/dist/src/security/RateLimiter.d.ts.map +1 -0
- package/dist/src/security/RateLimiter.js +782 -0
- package/dist/src/security/RateLimiter.js.map +1 -0
- package/dist/src/security/__tests__/pathValidation.test.d.ts +8 -0
- package/dist/src/security/__tests__/pathValidation.test.d.ts.map +1 -0
- package/dist/src/security/__tests__/pathValidation.test.js +249 -0
- package/dist/src/security/__tests__/pathValidation.test.js.map +1 -0
- package/dist/src/security/index.d.ts +18 -0
- package/dist/src/security/index.d.ts.map +1 -0
- package/dist/src/security/index.js +14 -0
- package/dist/src/security/index.js.map +1 -0
- package/dist/src/security/pathValidation.d.ts +95 -0
- package/dist/src/security/pathValidation.d.ts.map +1 -0
- package/dist/src/security/pathValidation.js +216 -0
- package/dist/src/security/pathValidation.js.map +1 -0
- package/dist/src/security/sanitization.d.ts +123 -0
- package/dist/src/security/sanitization.d.ts.map +1 -0
- package/dist/src/security/sanitization.js +378 -0
- package/dist/src/security/sanitization.js.map +1 -0
- package/dist/src/security/scanner.d.ts +151 -0
- package/dist/src/security/scanner.d.ts.map +1 -0
- package/dist/src/security/scanner.js +599 -0
- package/dist/src/security/scanner.js.map +1 -0
- package/dist/src/services/SearchService.d.ts +88 -0
- package/dist/src/services/SearchService.d.ts.map +1 -0
- package/dist/src/services/SearchService.js +305 -0
- package/dist/src/services/SearchService.js.map +1 -0
- package/dist/src/session/SessionContext.d.ts +116 -0
- package/dist/src/session/SessionContext.d.ts.map +1 -0
- package/dist/src/session/SessionContext.js +134 -0
- package/dist/src/session/SessionContext.js.map +1 -0
- package/dist/src/session/SessionHealthMonitor.d.ts +249 -0
- package/dist/src/session/SessionHealthMonitor.d.ts.map +1 -0
- package/dist/src/session/SessionHealthMonitor.js +302 -0
- package/dist/src/session/SessionHealthMonitor.js.map +1 -0
- package/dist/src/session/SessionManager.d.ts +179 -0
- package/dist/src/session/SessionManager.d.ts.map +1 -0
- package/dist/src/session/SessionManager.js +451 -0
- package/dist/src/session/SessionManager.js.map +1 -0
- package/dist/src/session/SessionRecovery.d.ts +84 -0
- package/dist/src/session/SessionRecovery.d.ts.map +1 -0
- package/dist/src/session/SessionRecovery.js +257 -0
- package/dist/src/session/SessionRecovery.js.map +1 -0
- package/dist/src/session/index.d.ts +40 -0
- package/dist/src/session/index.d.ts.map +1 -0
- package/dist/src/session/index.js +44 -0
- package/dist/src/session/index.js.map +1 -0
- package/dist/src/sources/BaseSourceAdapter.d.ts +144 -0
- package/dist/src/sources/BaseSourceAdapter.d.ts.map +1 -0
- package/dist/src/sources/BaseSourceAdapter.js +287 -0
- package/dist/src/sources/BaseSourceAdapter.js.map +1 -0
- package/dist/src/sources/GitHubSourceAdapter.d.ts +98 -0
- package/dist/src/sources/GitHubSourceAdapter.d.ts.map +1 -0
- package/dist/src/sources/GitHubSourceAdapter.js +269 -0
- package/dist/src/sources/GitHubSourceAdapter.js.map +1 -0
- package/dist/src/sources/GitLabSourceAdapter.d.ts +102 -0
- package/dist/src/sources/GitLabSourceAdapter.d.ts.map +1 -0
- package/dist/src/sources/GitLabSourceAdapter.js +310 -0
- package/dist/src/sources/GitLabSourceAdapter.js.map +1 -0
- package/dist/src/sources/ISourceAdapter.d.ts +110 -0
- package/dist/src/sources/ISourceAdapter.d.ts.map +1 -0
- package/dist/src/sources/ISourceAdapter.js +19 -0
- package/dist/src/sources/ISourceAdapter.js.map +1 -0
- package/dist/src/sources/LocalFilesystemAdapter.d.ts +112 -0
- package/dist/src/sources/LocalFilesystemAdapter.d.ts.map +1 -0
- package/dist/src/sources/LocalFilesystemAdapter.js +340 -0
- package/dist/src/sources/LocalFilesystemAdapter.js.map +1 -0
- package/dist/src/sources/RawUrlSourceAdapter.d.ts +128 -0
- package/dist/src/sources/RawUrlSourceAdapter.d.ts.map +1 -0
- package/dist/src/sources/RawUrlSourceAdapter.js +282 -0
- package/dist/src/sources/RawUrlSourceAdapter.js.map +1 -0
- package/dist/src/sources/SourceAdapterRegistry.d.ts +156 -0
- package/dist/src/sources/SourceAdapterRegistry.d.ts.map +1 -0
- package/dist/src/sources/SourceAdapterRegistry.js +242 -0
- package/dist/src/sources/SourceAdapterRegistry.js.map +1 -0
- package/dist/src/sources/SourceIndexer.d.ts +119 -0
- package/dist/src/sources/SourceIndexer.d.ts.map +1 -0
- package/dist/src/sources/SourceIndexer.js +285 -0
- package/dist/src/sources/SourceIndexer.js.map +1 -0
- package/dist/src/sources/index.d.ts +45 -0
- package/dist/src/sources/index.d.ts.map +1 -0
- package/dist/src/sources/index.js +51 -0
- package/dist/src/sources/index.js.map +1 -0
- package/dist/src/sources/shared.d.ts +125 -0
- package/dist/src/sources/shared.d.ts.map +1 -0
- package/dist/src/sources/shared.js +191 -0
- package/dist/src/sources/shared.js.map +1 -0
- package/dist/src/sources/types.d.ts +204 -0
- package/dist/src/sources/types.d.ts.map +1 -0
- package/dist/src/sources/types.js +6 -0
- package/dist/src/sources/types.js.map +1 -0
- package/dist/src/telemetry/index.d.ts +25 -0
- package/dist/src/telemetry/index.d.ts.map +1 -0
- package/dist/src/telemetry/index.js +32 -0
- package/dist/src/telemetry/index.js.map +1 -0
- package/dist/src/telemetry/metrics.d.ts +171 -0
- package/dist/src/telemetry/metrics.d.ts.map +1 -0
- package/dist/src/telemetry/metrics.js +401 -0
- package/dist/src/telemetry/metrics.js.map +1 -0
- package/dist/src/telemetry/prometheus.d.ts +81 -0
- package/dist/src/telemetry/prometheus.d.ts.map +1 -0
- package/dist/src/telemetry/prometheus.js +252 -0
- package/dist/src/telemetry/prometheus.js.map +1 -0
- package/dist/src/telemetry/tracer.d.ts +151 -0
- package/dist/src/telemetry/tracer.d.ts.map +1 -0
- package/dist/src/telemetry/tracer.js +391 -0
- package/dist/src/telemetry/tracer.js.map +1 -0
- package/dist/src/triggers/ContextScorer.d.ts +118 -0
- package/dist/src/triggers/ContextScorer.d.ts.map +1 -0
- package/dist/src/triggers/ContextScorer.js +265 -0
- package/dist/src/triggers/ContextScorer.js.map +1 -0
- package/dist/src/triggers/TriggerDetector.d.ts +178 -0
- package/dist/src/triggers/TriggerDetector.d.ts.map +1 -0
- package/dist/src/triggers/TriggerDetector.js +390 -0
- package/dist/src/triggers/TriggerDetector.js.map +1 -0
- package/dist/src/triggers/__tests__/ContextScorer.test.d.ts +6 -0
- package/dist/src/triggers/__tests__/ContextScorer.test.d.ts.map +1 -0
- package/dist/src/triggers/__tests__/ContextScorer.test.js +307 -0
- package/dist/src/triggers/__tests__/ContextScorer.test.js.map +1 -0
- package/dist/src/triggers/__tests__/TriggerDetector.test.d.ts +6 -0
- package/dist/src/triggers/__tests__/TriggerDetector.test.d.ts.map +1 -0
- package/dist/src/triggers/__tests__/TriggerDetector.test.js +249 -0
- package/dist/src/triggers/__tests__/TriggerDetector.test.js.map +1 -0
- package/dist/src/triggers/index.d.ts +8 -0
- package/dist/src/triggers/index.d.ts.map +1 -0
- package/dist/src/triggers/index.js +8 -0
- package/dist/src/triggers/index.js.map +1 -0
- package/dist/src/types/skill.d.ts +80 -0
- package/dist/src/types/skill.d.ts.map +1 -0
- package/dist/src/types/skill.js +5 -0
- package/dist/src/types/skill.js.map +1 -0
- package/dist/src/types.d.ts +88 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +13 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/index.d.ts +6 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +6 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/logger.d.ts +172 -0
- package/dist/src/utils/logger.d.ts.map +1 -0
- package/dist/src/utils/logger.js +291 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/dist/src/utils/retry.d.ts +97 -0
- package/dist/src/utils/retry.d.ts.map +1 -0
- package/dist/src/utils/retry.js +267 -0
- package/dist/src/utils/retry.js.map +1 -0
- package/dist/src/validation/index.d.ts +118 -0
- package/dist/src/validation/index.d.ts.map +1 -0
- package/dist/src/validation/index.js +434 -0
- package/dist/src/validation/index.js.map +1 -0
- package/dist/src/webhooks/WebhookHandler.d.ts +117 -0
- package/dist/src/webhooks/WebhookHandler.d.ts.map +1 -0
- package/dist/src/webhooks/WebhookHandler.js +349 -0
- package/dist/src/webhooks/WebhookHandler.js.map +1 -0
- package/dist/src/webhooks/WebhookPayload.d.ts +238 -0
- package/dist/src/webhooks/WebhookPayload.d.ts.map +1 -0
- package/dist/src/webhooks/WebhookPayload.js +244 -0
- package/dist/src/webhooks/WebhookPayload.js.map +1 -0
- package/dist/src/webhooks/WebhookQueue.d.ts +227 -0
- package/dist/src/webhooks/WebhookQueue.d.ts.map +1 -0
- package/dist/src/webhooks/WebhookQueue.js +328 -0
- package/dist/src/webhooks/WebhookQueue.js.map +1 -0
- package/dist/src/webhooks/index.d.ts +12 -0
- package/dist/src/webhooks/index.d.ts.map +1 -0
- package/dist/src/webhooks/index.js +15 -0
- package/dist/src/webhooks/index.js.map +1 -0
- package/dist/tests/Analytics.integration.test.d.ts +7 -0
- package/dist/tests/Analytics.integration.test.d.ts.map +1 -0
- package/dist/tests/Analytics.integration.test.js +367 -0
- package/dist/tests/Analytics.integration.test.js.map +1 -0
- package/dist/tests/AnalyticsRepository.test.d.ts +8 -0
- package/dist/tests/AnalyticsRepository.test.d.ts.map +1 -0
- package/dist/tests/AnalyticsRepository.test.js +399 -0
- package/dist/tests/AnalyticsRepository.test.js.map +1 -0
- package/dist/tests/AnalyticsStorage.test.d.ts +8 -0
- package/dist/tests/AnalyticsStorage.test.d.ts.map +1 -0
- package/dist/tests/AnalyticsStorage.test.js +271 -0
- package/dist/tests/AnalyticsStorage.test.js.map +1 -0
- package/dist/tests/AuditLogger.test.d.ts +8 -0
- package/dist/tests/AuditLogger.test.d.ts.map +1 -0
- package/dist/tests/AuditLogger.test.js +699 -0
- package/dist/tests/AuditLogger.test.js.map +1 -0
- package/dist/tests/BenchmarkRunner.test.d.ts +11 -0
- package/dist/tests/BenchmarkRunner.test.d.ts.map +1 -0
- package/dist/tests/BenchmarkRunner.test.js +641 -0
- package/dist/tests/BenchmarkRunner.test.js.map +1 -0
- package/dist/tests/CacheRepository.test.d.ts +5 -0
- package/dist/tests/CacheRepository.test.d.ts.map +1 -0
- package/dist/tests/CacheRepository.test.js +184 -0
- package/dist/tests/CacheRepository.test.js.map +1 -0
- package/dist/tests/CacheSecurity.test.d.ts +8 -0
- package/dist/tests/CacheSecurity.test.d.ts.map +1 -0
- package/dist/tests/CacheSecurity.test.js +273 -0
- package/dist/tests/CacheSecurity.test.js.map +1 -0
- package/dist/tests/CodebaseAnalyzer.test.d.ts +2 -0
- package/dist/tests/CodebaseAnalyzer.test.d.ts.map +1 -0
- package/dist/tests/CodebaseAnalyzer.test.js +347 -0
- package/dist/tests/CodebaseAnalyzer.test.js.map +1 -0
- package/dist/tests/DailyIndexPipeline.test.d.ts +7 -0
- package/dist/tests/DailyIndexPipeline.test.d.ts.map +1 -0
- package/dist/tests/DailyIndexPipeline.test.js +539 -0
- package/dist/tests/DailyIndexPipeline.test.js.map +1 -0
- package/dist/tests/EmbeddingService.test.d.ts +2 -0
- package/dist/tests/EmbeddingService.test.d.ts.map +1 -0
- package/dist/tests/EmbeddingService.test.js +252 -0
- package/dist/tests/EmbeddingService.test.js.map +1 -0
- package/dist/tests/ExperimentService.test.d.ts +7 -0
- package/dist/tests/ExperimentService.test.d.ts.map +1 -0
- package/dist/tests/ExperimentService.test.js +293 -0
- package/dist/tests/ExperimentService.test.js.map +1 -0
- package/dist/tests/GitHubIndexer.test.d.ts +10 -0
- package/dist/tests/GitHubIndexer.test.d.ts.map +1 -0
- package/dist/tests/GitHubIndexer.test.js +524 -0
- package/dist/tests/GitHubIndexer.test.js.map +1 -0
- package/dist/tests/GitHubSourceAdapter.test.d.ts +5 -0
- package/dist/tests/GitHubSourceAdapter.test.d.ts.map +1 -0
- package/dist/tests/GitHubSourceAdapter.test.js +385 -0
- package/dist/tests/GitHubSourceAdapter.test.js.map +1 -0
- package/dist/tests/MemoryProfiler.test.d.ts +12 -0
- package/dist/tests/MemoryProfiler.test.d.ts.map +1 -0
- package/dist/tests/MemoryProfiler.test.js +402 -0
- package/dist/tests/MemoryProfiler.test.js.map +1 -0
- package/dist/tests/OverlapDetector.test.d.ts +2 -0
- package/dist/tests/OverlapDetector.test.d.ts.map +1 -0
- package/dist/tests/OverlapDetector.test.js +340 -0
- package/dist/tests/OverlapDetector.test.js.map +1 -0
- package/dist/tests/QualityScorer.test.d.ts +7 -0
- package/dist/tests/QualityScorer.test.d.ts.map +1 -0
- package/dist/tests/QualityScorer.test.js +340 -0
- package/dist/tests/QualityScorer.test.js.map +1 -0
- package/dist/tests/QuarantineRepository.test.d.ts +7 -0
- package/dist/tests/QuarantineRepository.test.d.ts.map +1 -0
- package/dist/tests/QuarantineRepository.test.js +582 -0
- package/dist/tests/QuarantineRepository.test.js.map +1 -0
- package/dist/tests/ROIDashboardService.test.d.ts +7 -0
- package/dist/tests/ROIDashboardService.test.d.ts.map +1 -0
- package/dist/tests/ROIDashboardService.test.js +324 -0
- package/dist/tests/ROIDashboardService.test.js.map +1 -0
- package/dist/tests/RateLimiter.test.d.ts +7 -0
- package/dist/tests/RateLimiter.test.d.ts.map +1 -0
- package/dist/tests/RateLimiter.test.js +1017 -0
- package/dist/tests/RateLimiter.test.js.map +1 -0
- package/dist/tests/RawUrlSourceAdapter.security.test.d.ts +7 -0
- package/dist/tests/RawUrlSourceAdapter.security.test.d.ts.map +1 -0
- package/dist/tests/RawUrlSourceAdapter.security.test.js +455 -0
- package/dist/tests/RawUrlSourceAdapter.security.test.js.map +1 -0
- package/dist/tests/ScraperAdapters.test.d.ts +7 -0
- package/dist/tests/ScraperAdapters.test.d.ts.map +1 -0
- package/dist/tests/ScraperAdapters.test.js +748 -0
- package/dist/tests/ScraperAdapters.test.js.map +1 -0
- package/dist/tests/SearchQuality.test.d.ts +8 -0
- package/dist/tests/SearchQuality.test.d.ts.map +1 -0
- package/dist/tests/SearchQuality.test.js +397 -0
- package/dist/tests/SearchQuality.test.js.map +1 -0
- package/dist/tests/SearchService.test.d.ts +5 -0
- package/dist/tests/SearchService.test.d.ts.map +1 -0
- package/dist/tests/SearchService.test.js +218 -0
- package/dist/tests/SearchService.test.js.map +1 -0
- package/dist/tests/SecurityScanner.test.d.ts +6 -0
- package/dist/tests/SecurityScanner.test.d.ts.map +1 -0
- package/dist/tests/SecurityScanner.test.js +449 -0
- package/dist/tests/SecurityScanner.test.js.map +1 -0
- package/dist/tests/SessionHealthMonitor.test.d.ts +5 -0
- package/dist/tests/SessionHealthMonitor.test.d.ts.map +1 -0
- package/dist/tests/SessionHealthMonitor.test.js +449 -0
- package/dist/tests/SessionHealthMonitor.test.js.map +1 -0
- package/dist/tests/SessionManager.security.test.d.ts +10 -0
- package/dist/tests/SessionManager.security.test.d.ts.map +1 -0
- package/dist/tests/SessionManager.security.test.js +395 -0
- package/dist/tests/SessionManager.security.test.js.map +1 -0
- package/dist/tests/SessionManager.test.d.ts +8 -0
- package/dist/tests/SessionManager.test.d.ts.map +1 -0
- package/dist/tests/SessionManager.test.js +446 -0
- package/dist/tests/SessionManager.test.js.map +1 -0
- package/dist/tests/SkillMatcher.test.d.ts +2 -0
- package/dist/tests/SkillMatcher.test.d.ts.map +1 -0
- package/dist/tests/SkillMatcher.test.js +253 -0
- package/dist/tests/SkillMatcher.test.js.map +1 -0
- package/dist/tests/SkillRepository.test.d.ts +5 -0
- package/dist/tests/SkillRepository.test.d.ts.map +1 -0
- package/dist/tests/SkillRepository.test.js +237 -0
- package/dist/tests/SkillRepository.test.js.map +1 -0
- package/dist/tests/SwarmIndexer.test.d.ts +11 -0
- package/dist/tests/SwarmIndexer.test.d.ts.map +1 -0
- package/dist/tests/SwarmIndexer.test.js +374 -0
- package/dist/tests/SwarmIndexer.test.js.map +1 -0
- package/dist/tests/TieredCache.test.d.ts +7 -0
- package/dist/tests/TieredCache.test.d.ts.map +1 -0
- package/dist/tests/TieredCache.test.js +529 -0
- package/dist/tests/TieredCache.test.js.map +1 -0
- package/dist/tests/UsageAnalyticsService.test.d.ts +7 -0
- package/dist/tests/UsageAnalyticsService.test.d.ts.map +1 -0
- package/dist/tests/UsageAnalyticsService.test.js +238 -0
- package/dist/tests/UsageAnalyticsService.test.js.map +1 -0
- package/dist/tests/UsageTracker.test.d.ts +7 -0
- package/dist/tests/UsageTracker.test.d.ts.map +1 -0
- package/dist/tests/UsageTracker.test.js +196 -0
- package/dist/tests/UsageTracker.test.js.map +1 -0
- package/dist/tests/WebhookHandler.test.d.ts +10 -0
- package/dist/tests/WebhookHandler.test.d.ts.map +1 -0
- package/dist/tests/WebhookHandler.test.js +592 -0
- package/dist/tests/WebhookHandler.test.js.map +1 -0
- package/dist/tests/analytics/metrics-aggregator.test.d.ts +11 -0
- package/dist/tests/analytics/metrics-aggregator.test.d.ts.map +1 -0
- package/dist/tests/analytics/metrics-aggregator.test.js +273 -0
- package/dist/tests/analytics/metrics-aggregator.test.js.map +1 -0
- package/dist/tests/analytics/metrics-exporter.test.d.ts +11 -0
- package/dist/tests/analytics/metrics-exporter.test.d.ts.map +1 -0
- package/dist/tests/analytics/metrics-exporter.test.js +371 -0
- package/dist/tests/analytics/metrics-exporter.test.js.map +1 -0
- package/dist/tests/analytics/usage-tracker.test.d.ts +10 -0
- package/dist/tests/analytics/usage-tracker.test.d.ts.map +1 -0
- package/dist/tests/analytics/usage-tracker.test.js +151 -0
- package/dist/tests/analytics/usage-tracker.test.js.map +1 -0
- package/dist/tests/anonymizer.test.d.ts +8 -0
- package/dist/tests/anonymizer.test.d.ts.map +1 -0
- package/dist/tests/anonymizer.test.js +153 -0
- package/dist/tests/anonymizer.test.js.map +1 -0
- package/dist/tests/cache.test.d.ts +6 -0
- package/dist/tests/cache.test.d.ts.map +1 -0
- package/dist/tests/cache.test.js +170 -0
- package/dist/tests/cache.test.js.map +1 -0
- package/dist/tests/e2e/security/security.e2e.test.d.ts +8 -0
- package/dist/tests/e2e/security/security.e2e.test.d.ts.map +1 -0
- package/dist/tests/e2e/security/security.e2e.test.js +448 -0
- package/dist/tests/e2e/security/security.e2e.test.js.map +1 -0
- package/dist/tests/edge-cases/EdgeCases.test.d.ts +13 -0
- package/dist/tests/edge-cases/EdgeCases.test.d.ts.map +1 -0
- package/dist/tests/edge-cases/EdgeCases.test.js +844 -0
- package/dist/tests/edge-cases/EdgeCases.test.js.map +1 -0
- package/dist/tests/import-github-skills.test.d.ts +8 -0
- package/dist/tests/import-github-skills.test.d.ts.map +1 -0
- package/dist/tests/import-github-skills.test.js +390 -0
- package/dist/tests/import-github-skills.test.js.map +1 -0
- package/dist/tests/logger.test.d.ts +2 -0
- package/dist/tests/logger.test.d.ts.map +1 -0
- package/dist/tests/logger.test.js +417 -0
- package/dist/tests/logger.test.js.map +1 -0
- package/dist/tests/performance/LargeScalePerformance.test.d.ts +14 -0
- package/dist/tests/performance/LargeScalePerformance.test.d.ts.map +1 -0
- package/dist/tests/performance/LargeScalePerformance.test.js +558 -0
- package/dist/tests/performance/LargeScalePerformance.test.js.map +1 -0
- package/dist/tests/retry.test.d.ts +7 -0
- package/dist/tests/retry.test.d.ts.map +1 -0
- package/dist/tests/retry.test.js +302 -0
- package/dist/tests/retry.test.js.map +1 -0
- package/dist/tests/sanitization.test.d.ts +8 -0
- package/dist/tests/sanitization.test.d.ts.map +1 -0
- package/dist/tests/sanitization.test.js +413 -0
- package/dist/tests/sanitization.test.js.map +1 -0
- package/dist/tests/schema.test.d.ts +5 -0
- package/dist/tests/schema.test.d.ts.map +1 -0
- package/dist/tests/schema.test.js +167 -0
- package/dist/tests/schema.test.js.map +1 -0
- package/dist/tests/scripts/import-to-database.test.d.ts +11 -0
- package/dist/tests/scripts/import-to-database.test.d.ts.map +1 -0
- package/dist/tests/scripts/import-to-database.test.js +325 -0
- package/dist/tests/scripts/import-to-database.test.js.map +1 -0
- package/dist/tests/security/ContinuousSecurity.test.d.ts +6 -0
- package/dist/tests/security/ContinuousSecurity.test.d.ts.map +1 -0
- package/dist/tests/security/ContinuousSecurity.test.js +595 -0
- package/dist/tests/security/ContinuousSecurity.test.js.map +1 -0
- package/dist/tests/security/ReDoS.test.d.ts +8 -0
- package/dist/tests/security/ReDoS.test.d.ts.map +1 -0
- package/dist/tests/security/ReDoS.test.js +213 -0
- package/dist/tests/security/ReDoS.test.js.map +1 -0
- package/dist/tests/security.test.d.ts +5 -0
- package/dist/tests/security.test.d.ts.map +1 -0
- package/dist/tests/security.test.js +134 -0
- package/dist/tests/security.test.js.map +1 -0
- package/dist/tests/shared.test.d.ts +7 -0
- package/dist/tests/shared.test.d.ts.map +1 -0
- package/dist/tests/shared.test.js +480 -0
- package/dist/tests/shared.test.js.map +1 -0
- package/dist/tests/sources.test.d.ts +5 -0
- package/dist/tests/sources.test.d.ts.map +1 -0
- package/dist/tests/sources.test.js +369 -0
- package/dist/tests/sources.test.js.map +1 -0
- package/dist/tests/stats.test.d.ts +11 -0
- package/dist/tests/stats.test.d.ts.map +1 -0
- package/dist/tests/stats.test.js +124 -0
- package/dist/tests/stats.test.js.map +1 -0
- package/dist/tests/telemetry.test.d.ts +11 -0
- package/dist/tests/telemetry.test.d.ts.map +1 -0
- package/dist/tests/telemetry.test.js +424 -0
- package/dist/tests/telemetry.test.js.map +1 -0
- package/dist/tests/test-utils.d.ts +74 -0
- package/dist/tests/test-utils.d.ts.map +1 -0
- package/dist/tests/test-utils.js +98 -0
- package/dist/tests/test-utils.js.map +1 -0
- package/dist/tests/validate-skills.test.d.ts +5 -0
- package/dist/tests/validate-skills.test.d.ts.map +1 -0
- package/dist/tests/validate-skills.test.js +649 -0
- package/dist/tests/validate-skills.test.js.map +1 -0
- package/dist/tests/validation.test.d.ts +7 -0
- package/dist/tests/validation.test.d.ts.map +1 -0
- package/dist/tests/validation.test.js +495 -0
- package/dist/tests/validation.test.js.map +1 -0
- package/dist/tests/webhooks/WebhookHandler.idempotency.test.d.ts +8 -0
- package/dist/tests/webhooks/WebhookHandler.idempotency.test.d.ts.map +1 -0
- package/dist/tests/webhooks/WebhookHandler.idempotency.test.js +190 -0
- package/dist/tests/webhooks/WebhookHandler.idempotency.test.js.map +1 -0
- package/dist/tests/webhooks/WebhookPayload.security.test.d.ts +8 -0
- package/dist/tests/webhooks/WebhookPayload.security.test.d.ts.map +1 -0
- package/dist/tests/webhooks/WebhookPayload.security.test.js +204 -0
- package/dist/tests/webhooks/WebhookPayload.security.test.js.map +1 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +13 -0
- package/dist/vitest.config.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiter Tests - SMI-730
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive tests for token bucket rate limiting.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
7
|
+
import { RateLimiter, InMemoryRateLimitStorage, RATE_LIMIT_PRESETS, createRateLimiterFromPreset, RateLimitQueueTimeoutError, RateLimitQueueFullError, } from '../src/security/RateLimiter.js';
|
|
8
|
+
describe('RateLimiter - Token Bucket Algorithm', () => {
|
|
9
|
+
let limiter;
|
|
10
|
+
let storage;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
storage = new InMemoryRateLimitStorage();
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
storage.dispose();
|
|
16
|
+
if (limiter) {
|
|
17
|
+
limiter.dispose();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
describe('Basic Rate Limiting', () => {
|
|
21
|
+
it('should allow requests within limit', async () => {
|
|
22
|
+
limiter = new RateLimiter({
|
|
23
|
+
maxTokens: 10,
|
|
24
|
+
refillRate: 1,
|
|
25
|
+
windowMs: 60000,
|
|
26
|
+
}, storage);
|
|
27
|
+
// First 10 requests should succeed
|
|
28
|
+
for (let i = 0; i < 10; i++) {
|
|
29
|
+
const result = await limiter.checkLimit('test-key');
|
|
30
|
+
expect(result.allowed).toBe(true);
|
|
31
|
+
expect(result.remaining).toBe(9 - i);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
it('should deny requests exceeding limit', async () => {
|
|
35
|
+
limiter = new RateLimiter({
|
|
36
|
+
maxTokens: 5,
|
|
37
|
+
refillRate: 1,
|
|
38
|
+
windowMs: 60000,
|
|
39
|
+
}, storage);
|
|
40
|
+
// Use up all tokens
|
|
41
|
+
for (let i = 0; i < 5; i++) {
|
|
42
|
+
await limiter.checkLimit('test-key');
|
|
43
|
+
}
|
|
44
|
+
// Next request should be denied
|
|
45
|
+
const result = await limiter.checkLimit('test-key');
|
|
46
|
+
expect(result.allowed).toBe(false);
|
|
47
|
+
expect(result.remaining).toBe(0);
|
|
48
|
+
expect(result.retryAfterMs).toBeGreaterThan(0);
|
|
49
|
+
expect(result.resetAt).toBeDefined();
|
|
50
|
+
});
|
|
51
|
+
it('should track different keys independently', async () => {
|
|
52
|
+
limiter = new RateLimiter({
|
|
53
|
+
maxTokens: 3,
|
|
54
|
+
refillRate: 1,
|
|
55
|
+
windowMs: 60000,
|
|
56
|
+
}, storage);
|
|
57
|
+
// Use all tokens for key1
|
|
58
|
+
for (let i = 0; i < 3; i++) {
|
|
59
|
+
await limiter.checkLimit('key1');
|
|
60
|
+
}
|
|
61
|
+
// key1 should be rate limited
|
|
62
|
+
const result1 = await limiter.checkLimit('key1');
|
|
63
|
+
expect(result1.allowed).toBe(false);
|
|
64
|
+
// key2 should still have tokens
|
|
65
|
+
const result2 = await limiter.checkLimit('key2');
|
|
66
|
+
expect(result2.allowed).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('Token Refill', () => {
|
|
70
|
+
it('should refill tokens over time', async () => {
|
|
71
|
+
limiter = new RateLimiter({
|
|
72
|
+
maxTokens: 10,
|
|
73
|
+
refillRate: 10, // 10 tokens per second
|
|
74
|
+
windowMs: 60000,
|
|
75
|
+
}, storage);
|
|
76
|
+
// Use all tokens
|
|
77
|
+
for (let i = 0; i < 10; i++) {
|
|
78
|
+
await limiter.checkLimit('test-key');
|
|
79
|
+
}
|
|
80
|
+
// Should be rate limited
|
|
81
|
+
let result = await limiter.checkLimit('test-key');
|
|
82
|
+
expect(result.allowed).toBe(false);
|
|
83
|
+
// Wait 500ms (should refill ~5 tokens)
|
|
84
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
85
|
+
// Should have ~5 tokens now
|
|
86
|
+
result = await limiter.checkLimit('test-key');
|
|
87
|
+
expect(result.allowed).toBe(true);
|
|
88
|
+
expect(result.remaining).toBeGreaterThanOrEqual(3);
|
|
89
|
+
expect(result.remaining).toBeLessThanOrEqual(5);
|
|
90
|
+
});
|
|
91
|
+
it('should not exceed max tokens on refill', async () => {
|
|
92
|
+
limiter = new RateLimiter({
|
|
93
|
+
maxTokens: 5,
|
|
94
|
+
refillRate: 10, // Fast refill
|
|
95
|
+
windowMs: 60000,
|
|
96
|
+
}, storage);
|
|
97
|
+
// Use one token
|
|
98
|
+
await limiter.checkLimit('test-key');
|
|
99
|
+
// Wait for refill
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
101
|
+
// Check remaining - should cap at maxTokens
|
|
102
|
+
const result = await limiter.checkLimit('test-key');
|
|
103
|
+
expect(result.remaining).toBeLessThanOrEqual(4);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe('Token Cost', () => {
|
|
107
|
+
it('should consume multiple tokens per request', async () => {
|
|
108
|
+
limiter = new RateLimiter({
|
|
109
|
+
maxTokens: 10,
|
|
110
|
+
refillRate: 1,
|
|
111
|
+
windowMs: 60000,
|
|
112
|
+
}, storage);
|
|
113
|
+
// Request costing 5 tokens
|
|
114
|
+
const result1 = await limiter.checkLimit('test-key', 5);
|
|
115
|
+
expect(result1.allowed).toBe(true);
|
|
116
|
+
expect(result1.remaining).toBe(5);
|
|
117
|
+
// Request costing 3 tokens
|
|
118
|
+
const result2 = await limiter.checkLimit('test-key', 3);
|
|
119
|
+
expect(result2.allowed).toBe(true);
|
|
120
|
+
expect(result2.remaining).toBe(2);
|
|
121
|
+
// Request costing 5 tokens - should fail
|
|
122
|
+
const result3 = await limiter.checkLimit('test-key', 5);
|
|
123
|
+
expect(result3.allowed).toBe(false);
|
|
124
|
+
expect(result3.remaining).toBe(2);
|
|
125
|
+
});
|
|
126
|
+
it('should calculate correct retry time for high cost', async () => {
|
|
127
|
+
limiter = new RateLimiter({
|
|
128
|
+
maxTokens: 10,
|
|
129
|
+
refillRate: 2, // 2 tokens per second
|
|
130
|
+
windowMs: 60000,
|
|
131
|
+
}, storage);
|
|
132
|
+
// Use all tokens
|
|
133
|
+
await limiter.checkLimit('test-key', 10);
|
|
134
|
+
// Request costing 4 tokens - need to wait ~2 seconds
|
|
135
|
+
const result = await limiter.checkLimit('test-key', 4);
|
|
136
|
+
expect(result.allowed).toBe(false);
|
|
137
|
+
expect(result.retryAfterMs).toBeGreaterThanOrEqual(1900);
|
|
138
|
+
expect(result.retryAfterMs).toBeLessThanOrEqual(2100);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
describe('Reset Functionality', () => {
|
|
142
|
+
it('should reset rate limit for a key', async () => {
|
|
143
|
+
limiter = new RateLimiter({
|
|
144
|
+
maxTokens: 3,
|
|
145
|
+
refillRate: 1,
|
|
146
|
+
windowMs: 60000,
|
|
147
|
+
}, storage);
|
|
148
|
+
// Use all tokens
|
|
149
|
+
for (let i = 0; i < 3; i++) {
|
|
150
|
+
await limiter.checkLimit('test-key');
|
|
151
|
+
}
|
|
152
|
+
// Should be rate limited
|
|
153
|
+
let result = await limiter.checkLimit('test-key');
|
|
154
|
+
expect(result.allowed).toBe(false);
|
|
155
|
+
// Reset
|
|
156
|
+
await limiter.reset('test-key');
|
|
157
|
+
// Should have full tokens again
|
|
158
|
+
result = await limiter.checkLimit('test-key');
|
|
159
|
+
expect(result.allowed).toBe(true);
|
|
160
|
+
expect(result.remaining).toBe(2);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe('State Inspection', () => {
|
|
164
|
+
it('should return current bucket state', async () => {
|
|
165
|
+
limiter = new RateLimiter({
|
|
166
|
+
maxTokens: 10,
|
|
167
|
+
refillRate: 1,
|
|
168
|
+
windowMs: 60000,
|
|
169
|
+
}, storage);
|
|
170
|
+
// Make some requests
|
|
171
|
+
await limiter.checkLimit('test-key');
|
|
172
|
+
await limiter.checkLimit('test-key');
|
|
173
|
+
// Get state
|
|
174
|
+
const state = await limiter.getState('test-key');
|
|
175
|
+
expect(state).toBeDefined();
|
|
176
|
+
expect(state?.tokens).toBeCloseTo(8, 0);
|
|
177
|
+
expect(state?.lastRefill).toBeGreaterThan(0);
|
|
178
|
+
});
|
|
179
|
+
it('should return null for non-existent key', async () => {
|
|
180
|
+
limiter = new RateLimiter({
|
|
181
|
+
maxTokens: 10,
|
|
182
|
+
refillRate: 1,
|
|
183
|
+
windowMs: 60000,
|
|
184
|
+
}, storage);
|
|
185
|
+
const state = await limiter.getState('non-existent');
|
|
186
|
+
expect(state).toBeNull();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
describe('Error Handling', () => {
|
|
190
|
+
it('should gracefully handle storage errors with fail-open (default)', async () => {
|
|
191
|
+
// Mock storage that throws errors
|
|
192
|
+
const errorStorage = {
|
|
193
|
+
get: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
194
|
+
set: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
195
|
+
delete: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
196
|
+
};
|
|
197
|
+
limiter = new RateLimiter({
|
|
198
|
+
maxTokens: 10,
|
|
199
|
+
refillRate: 1,
|
|
200
|
+
windowMs: 60000,
|
|
201
|
+
}, errorStorage);
|
|
202
|
+
// Should allow request despite error (graceful degradation)
|
|
203
|
+
const result = await limiter.checkLimit('test-key');
|
|
204
|
+
expect(result.allowed).toBe(true);
|
|
205
|
+
expect(result.remaining).toBe(10);
|
|
206
|
+
expect(result.limit).toBe(10);
|
|
207
|
+
});
|
|
208
|
+
it('should deny requests with fail-closed mode on storage errors', async () => {
|
|
209
|
+
// Mock storage that throws errors
|
|
210
|
+
const errorStorage = {
|
|
211
|
+
get: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
212
|
+
set: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
213
|
+
delete: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
214
|
+
};
|
|
215
|
+
limiter = new RateLimiter({
|
|
216
|
+
maxTokens: 10,
|
|
217
|
+
refillRate: 1,
|
|
218
|
+
windowMs: 60000,
|
|
219
|
+
failMode: 'closed',
|
|
220
|
+
}, errorStorage);
|
|
221
|
+
// Should deny request due to fail-closed mode
|
|
222
|
+
const result = await limiter.checkLimit('test-key');
|
|
223
|
+
expect(result.allowed).toBe(false);
|
|
224
|
+
expect(result.remaining).toBe(0);
|
|
225
|
+
expect(result.limit).toBe(10);
|
|
226
|
+
expect(result.retryAfterMs).toBe(60000);
|
|
227
|
+
expect(result.resetAt).toBeDefined();
|
|
228
|
+
});
|
|
229
|
+
it('should explicitly allow requests with fail-open mode on storage errors', async () => {
|
|
230
|
+
// Mock storage that throws errors
|
|
231
|
+
const errorStorage = {
|
|
232
|
+
get: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
233
|
+
set: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
234
|
+
delete: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
235
|
+
};
|
|
236
|
+
limiter = new RateLimiter({
|
|
237
|
+
maxTokens: 10,
|
|
238
|
+
refillRate: 1,
|
|
239
|
+
windowMs: 60000,
|
|
240
|
+
failMode: 'open',
|
|
241
|
+
}, errorStorage);
|
|
242
|
+
// Should allow request due to fail-open mode
|
|
243
|
+
const result = await limiter.checkLimit('test-key');
|
|
244
|
+
expect(result.allowed).toBe(true);
|
|
245
|
+
expect(result.remaining).toBe(10);
|
|
246
|
+
expect(result.limit).toBe(10);
|
|
247
|
+
});
|
|
248
|
+
it('should use fail-closed for STRICT preset', async () => {
|
|
249
|
+
// Mock storage that throws errors
|
|
250
|
+
const errorStorage = {
|
|
251
|
+
get: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
252
|
+
set: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
253
|
+
delete: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
254
|
+
};
|
|
255
|
+
limiter = new RateLimiter(RATE_LIMIT_PRESETS.STRICT, errorStorage);
|
|
256
|
+
// STRICT preset uses fail-closed, so should deny on error
|
|
257
|
+
const result = await limiter.checkLimit('test-key');
|
|
258
|
+
expect(result.allowed).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
it('should use fail-open for STANDARD preset', async () => {
|
|
261
|
+
// Mock storage that throws errors
|
|
262
|
+
const errorStorage = {
|
|
263
|
+
get: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
264
|
+
set: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
265
|
+
delete: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
266
|
+
};
|
|
267
|
+
limiter = new RateLimiter(RATE_LIMIT_PRESETS.STANDARD, errorStorage);
|
|
268
|
+
// STANDARD preset uses fail-open, so should allow on error
|
|
269
|
+
const result = await limiter.checkLimit('test-key');
|
|
270
|
+
expect(result.allowed).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
describe('Key Prefix', () => {
|
|
274
|
+
it('should use custom key prefix', async () => {
|
|
275
|
+
limiter = new RateLimiter({
|
|
276
|
+
maxTokens: 10,
|
|
277
|
+
refillRate: 1,
|
|
278
|
+
windowMs: 60000,
|
|
279
|
+
keyPrefix: 'custom',
|
|
280
|
+
}, storage);
|
|
281
|
+
await limiter.checkLimit('test-key');
|
|
282
|
+
const stats = storage.getStats();
|
|
283
|
+
expect(stats.keys[0]).toContain('custom:test-key');
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
describe('Presets', () => {
|
|
287
|
+
it('should create limiter from STRICT preset', () => {
|
|
288
|
+
limiter = createRateLimiterFromPreset('STRICT', storage);
|
|
289
|
+
expect(limiter).toBeInstanceOf(RateLimiter);
|
|
290
|
+
});
|
|
291
|
+
it('should create limiter from STANDARD preset', () => {
|
|
292
|
+
limiter = createRateLimiterFromPreset('STANDARD', storage);
|
|
293
|
+
expect(limiter).toBeInstanceOf(RateLimiter);
|
|
294
|
+
});
|
|
295
|
+
it('should create limiter from RELAXED preset', () => {
|
|
296
|
+
limiter = createRateLimiterFromPreset('RELAXED', storage);
|
|
297
|
+
expect(limiter).toBeInstanceOf(RateLimiter);
|
|
298
|
+
});
|
|
299
|
+
it('should create limiter from GENEROUS preset', () => {
|
|
300
|
+
limiter = createRateLimiterFromPreset('GENEROUS', storage);
|
|
301
|
+
expect(limiter).toBeInstanceOf(RateLimiter);
|
|
302
|
+
});
|
|
303
|
+
it('should create limiter from HIGH_THROUGHPUT preset', () => {
|
|
304
|
+
limiter = createRateLimiterFromPreset('HIGH_THROUGHPUT', storage);
|
|
305
|
+
expect(limiter).toBeInstanceOf(RateLimiter);
|
|
306
|
+
});
|
|
307
|
+
it('should enforce STRICT limits correctly', async () => {
|
|
308
|
+
limiter = createRateLimiterFromPreset('STRICT', storage);
|
|
309
|
+
// STRICT allows 10 requests per minute
|
|
310
|
+
for (let i = 0; i < 10; i++) {
|
|
311
|
+
const result = await limiter.checkLimit('test-key');
|
|
312
|
+
expect(result.allowed).toBe(true);
|
|
313
|
+
}
|
|
314
|
+
// 11th request should fail
|
|
315
|
+
const result = await limiter.checkLimit('test-key');
|
|
316
|
+
expect(result.allowed).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
describe('Metrics Tracking - SMI-752', () => {
|
|
320
|
+
it('should accumulate metrics for allowed requests', async () => {
|
|
321
|
+
limiter = new RateLimiter({
|
|
322
|
+
maxTokens: 10,
|
|
323
|
+
refillRate: 1,
|
|
324
|
+
windowMs: 60000,
|
|
325
|
+
}, storage);
|
|
326
|
+
// Make 5 allowed requests
|
|
327
|
+
for (let i = 0; i < 5; i++) {
|
|
328
|
+
await limiter.checkLimit('test-key');
|
|
329
|
+
}
|
|
330
|
+
const metrics = limiter.getMetrics('test-key');
|
|
331
|
+
expect(metrics).toBeDefined();
|
|
332
|
+
expect(metrics.allowed).toBe(5);
|
|
333
|
+
expect(metrics.blocked).toBe(0);
|
|
334
|
+
expect(metrics.errors).toBe(0);
|
|
335
|
+
expect(metrics.lastReset).toBeInstanceOf(Date);
|
|
336
|
+
});
|
|
337
|
+
it('should accumulate metrics for blocked requests', async () => {
|
|
338
|
+
limiter = new RateLimiter({
|
|
339
|
+
maxTokens: 3,
|
|
340
|
+
refillRate: 0.1,
|
|
341
|
+
windowMs: 60000,
|
|
342
|
+
}, storage);
|
|
343
|
+
// Make 3 allowed requests
|
|
344
|
+
for (let i = 0; i < 3; i++) {
|
|
345
|
+
await limiter.checkLimit('test-key');
|
|
346
|
+
}
|
|
347
|
+
// Make 2 blocked requests
|
|
348
|
+
await limiter.checkLimit('test-key');
|
|
349
|
+
await limiter.checkLimit('test-key');
|
|
350
|
+
const metrics = limiter.getMetrics('test-key');
|
|
351
|
+
expect(metrics.allowed).toBe(3);
|
|
352
|
+
expect(metrics.blocked).toBe(2);
|
|
353
|
+
expect(metrics.errors).toBe(0);
|
|
354
|
+
});
|
|
355
|
+
it('should include metrics in result', async () => {
|
|
356
|
+
limiter = new RateLimiter({
|
|
357
|
+
maxTokens: 5,
|
|
358
|
+
refillRate: 1,
|
|
359
|
+
windowMs: 60000,
|
|
360
|
+
}, storage);
|
|
361
|
+
const result = await limiter.checkLimit('test-key');
|
|
362
|
+
expect(result.metrics).toBeDefined();
|
|
363
|
+
expect(result.metrics?.allowed).toBe(1);
|
|
364
|
+
});
|
|
365
|
+
it('should track errors in metrics', async () => {
|
|
366
|
+
const errorStorage = {
|
|
367
|
+
get: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
368
|
+
set: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
369
|
+
delete: vi.fn().mockRejectedValue(new Error('Storage error')),
|
|
370
|
+
};
|
|
371
|
+
limiter = new RateLimiter({
|
|
372
|
+
maxTokens: 10,
|
|
373
|
+
refillRate: 1,
|
|
374
|
+
windowMs: 60000,
|
|
375
|
+
failMode: 'open',
|
|
376
|
+
}, errorStorage);
|
|
377
|
+
// Make 3 requests that will error
|
|
378
|
+
for (let i = 0; i < 3; i++) {
|
|
379
|
+
await limiter.checkLimit('test-key');
|
|
380
|
+
}
|
|
381
|
+
const metrics = limiter.getMetrics('test-key');
|
|
382
|
+
expect(metrics.errors).toBe(3);
|
|
383
|
+
// Fail-open counts as allowed
|
|
384
|
+
expect(metrics.allowed).toBe(3);
|
|
385
|
+
});
|
|
386
|
+
it('should getMetrics() return all metrics when no key specified', async () => {
|
|
387
|
+
limiter = new RateLimiter({
|
|
388
|
+
maxTokens: 10,
|
|
389
|
+
refillRate: 1,
|
|
390
|
+
windowMs: 60000,
|
|
391
|
+
}, storage);
|
|
392
|
+
// Make requests for multiple keys
|
|
393
|
+
await limiter.checkLimit('key1');
|
|
394
|
+
await limiter.checkLimit('key1');
|
|
395
|
+
await limiter.checkLimit('key2');
|
|
396
|
+
await limiter.checkLimit('key3');
|
|
397
|
+
const allMetrics = limiter.getMetrics();
|
|
398
|
+
expect(allMetrics).toBeInstanceOf(Map);
|
|
399
|
+
expect(allMetrics.size).toBe(3);
|
|
400
|
+
expect(allMetrics.get('key1')?.allowed).toBe(2);
|
|
401
|
+
expect(allMetrics.get('key2')?.allowed).toBe(1);
|
|
402
|
+
expect(allMetrics.get('key3')?.allowed).toBe(1);
|
|
403
|
+
});
|
|
404
|
+
it('should resetMetrics() clear data for specific key', async () => {
|
|
405
|
+
limiter = new RateLimiter({
|
|
406
|
+
maxTokens: 10,
|
|
407
|
+
refillRate: 1,
|
|
408
|
+
windowMs: 60000,
|
|
409
|
+
}, storage);
|
|
410
|
+
// Make requests
|
|
411
|
+
await limiter.checkLimit('key1');
|
|
412
|
+
await limiter.checkLimit('key2');
|
|
413
|
+
// Reset only key1
|
|
414
|
+
limiter.resetMetrics('key1');
|
|
415
|
+
expect(limiter.getMetrics('key1')).toBeUndefined();
|
|
416
|
+
expect(limiter.getMetrics('key2')).toBeDefined();
|
|
417
|
+
});
|
|
418
|
+
it('should resetMetrics() clear all data when no key specified', async () => {
|
|
419
|
+
limiter = new RateLimiter({
|
|
420
|
+
maxTokens: 10,
|
|
421
|
+
refillRate: 1,
|
|
422
|
+
windowMs: 60000,
|
|
423
|
+
}, storage);
|
|
424
|
+
// Make requests for multiple keys
|
|
425
|
+
await limiter.checkLimit('key1');
|
|
426
|
+
await limiter.checkLimit('key2');
|
|
427
|
+
await limiter.checkLimit('key3');
|
|
428
|
+
// Reset all
|
|
429
|
+
limiter.resetMetrics();
|
|
430
|
+
const allMetrics = limiter.getMetrics();
|
|
431
|
+
expect(allMetrics.size).toBe(0);
|
|
432
|
+
});
|
|
433
|
+
it('should call onLimitExceeded callback when limit is exceeded', async () => {
|
|
434
|
+
const onLimitExceeded = vi.fn();
|
|
435
|
+
limiter = new RateLimiter({
|
|
436
|
+
maxTokens: 3,
|
|
437
|
+
refillRate: 0.1,
|
|
438
|
+
windowMs: 60000,
|
|
439
|
+
onLimitExceeded,
|
|
440
|
+
}, storage);
|
|
441
|
+
// Use up all tokens
|
|
442
|
+
for (let i = 0; i < 3; i++) {
|
|
443
|
+
await limiter.checkLimit('test-key');
|
|
444
|
+
}
|
|
445
|
+
// This should trigger the callback
|
|
446
|
+
await limiter.checkLimit('test-key');
|
|
447
|
+
expect(onLimitExceeded).toHaveBeenCalledTimes(1);
|
|
448
|
+
expect(onLimitExceeded).toHaveBeenCalledWith('test-key', expect.objectContaining({
|
|
449
|
+
allowed: 3,
|
|
450
|
+
blocked: 1,
|
|
451
|
+
errors: 0,
|
|
452
|
+
}));
|
|
453
|
+
});
|
|
454
|
+
it('should call onLimitExceeded callback on each exceeded request', async () => {
|
|
455
|
+
const onLimitExceeded = vi.fn();
|
|
456
|
+
limiter = new RateLimiter({
|
|
457
|
+
maxTokens: 2,
|
|
458
|
+
refillRate: 0.01,
|
|
459
|
+
windowMs: 60000,
|
|
460
|
+
onLimitExceeded,
|
|
461
|
+
}, storage);
|
|
462
|
+
// Use up all tokens
|
|
463
|
+
await limiter.checkLimit('test-key');
|
|
464
|
+
await limiter.checkLimit('test-key');
|
|
465
|
+
// These should trigger callbacks
|
|
466
|
+
await limiter.checkLimit('test-key');
|
|
467
|
+
await limiter.checkLimit('test-key');
|
|
468
|
+
await limiter.checkLimit('test-key');
|
|
469
|
+
expect(onLimitExceeded).toHaveBeenCalledTimes(3);
|
|
470
|
+
});
|
|
471
|
+
it('should not call onLimitExceeded for allowed requests', async () => {
|
|
472
|
+
const onLimitExceeded = vi.fn();
|
|
473
|
+
limiter = new RateLimiter({
|
|
474
|
+
maxTokens: 10,
|
|
475
|
+
refillRate: 1,
|
|
476
|
+
windowMs: 60000,
|
|
477
|
+
onLimitExceeded,
|
|
478
|
+
}, storage);
|
|
479
|
+
// Make allowed requests
|
|
480
|
+
for (let i = 0; i < 5; i++) {
|
|
481
|
+
await limiter.checkLimit('test-key');
|
|
482
|
+
}
|
|
483
|
+
expect(onLimitExceeded).not.toHaveBeenCalled();
|
|
484
|
+
});
|
|
485
|
+
it('should clear metrics on dispose', async () => {
|
|
486
|
+
limiter = new RateLimiter({
|
|
487
|
+
maxTokens: 10,
|
|
488
|
+
refillRate: 1,
|
|
489
|
+
windowMs: 60000,
|
|
490
|
+
}, storage);
|
|
491
|
+
// Make requests
|
|
492
|
+
await limiter.checkLimit('test-key');
|
|
493
|
+
// Dispose should clear metrics
|
|
494
|
+
limiter.dispose();
|
|
495
|
+
const allMetrics = limiter.getMetrics();
|
|
496
|
+
expect(allMetrics.size).toBe(0);
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
describe('InMemoryRateLimitStorage', () => {
|
|
501
|
+
let storage;
|
|
502
|
+
beforeEach(() => {
|
|
503
|
+
storage = new InMemoryRateLimitStorage();
|
|
504
|
+
});
|
|
505
|
+
afterEach(() => {
|
|
506
|
+
storage.dispose();
|
|
507
|
+
});
|
|
508
|
+
describe('Basic Operations', () => {
|
|
509
|
+
it('should store and retrieve bucket', async () => {
|
|
510
|
+
const bucket = {
|
|
511
|
+
tokens: 10,
|
|
512
|
+
lastRefill: Date.now(),
|
|
513
|
+
firstRequest: Date.now(),
|
|
514
|
+
};
|
|
515
|
+
await storage.set('test-key', bucket, 60000);
|
|
516
|
+
const retrieved = await storage.get('test-key');
|
|
517
|
+
expect(retrieved).toEqual(bucket);
|
|
518
|
+
});
|
|
519
|
+
it('should return null for non-existent key', async () => {
|
|
520
|
+
const result = await storage.get('non-existent');
|
|
521
|
+
expect(result).toBeNull();
|
|
522
|
+
});
|
|
523
|
+
it('should delete key', async () => {
|
|
524
|
+
const bucket = {
|
|
525
|
+
tokens: 10,
|
|
526
|
+
lastRefill: Date.now(),
|
|
527
|
+
firstRequest: Date.now(),
|
|
528
|
+
};
|
|
529
|
+
await storage.set('test-key', bucket, 60000);
|
|
530
|
+
await storage.delete('test-key');
|
|
531
|
+
const result = await storage.get('test-key');
|
|
532
|
+
expect(result).toBeNull();
|
|
533
|
+
});
|
|
534
|
+
it('should clear all keys', async () => {
|
|
535
|
+
const bucket = {
|
|
536
|
+
tokens: 10,
|
|
537
|
+
lastRefill: Date.now(),
|
|
538
|
+
firstRequest: Date.now(),
|
|
539
|
+
};
|
|
540
|
+
await storage.set('key1', bucket, 60000);
|
|
541
|
+
await storage.set('key2', bucket, 60000);
|
|
542
|
+
await storage.clear?.();
|
|
543
|
+
const stats = storage.getStats();
|
|
544
|
+
expect(stats.size).toBe(0);
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
describe('TTL and Expiration', () => {
|
|
548
|
+
it('should expire entries after TTL', async () => {
|
|
549
|
+
const bucket = {
|
|
550
|
+
tokens: 10,
|
|
551
|
+
lastRefill: Date.now(),
|
|
552
|
+
firstRequest: Date.now(),
|
|
553
|
+
};
|
|
554
|
+
// Set with 100ms TTL
|
|
555
|
+
await storage.set('test-key', bucket, 100);
|
|
556
|
+
// Should exist immediately
|
|
557
|
+
let result = await storage.get('test-key');
|
|
558
|
+
expect(result).toEqual(bucket);
|
|
559
|
+
// Wait for expiration
|
|
560
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
561
|
+
// Should be expired
|
|
562
|
+
result = await storage.get('test-key');
|
|
563
|
+
expect(result).toBeNull();
|
|
564
|
+
});
|
|
565
|
+
it('should clean up expired entries periodically', async () => {
|
|
566
|
+
// Create storage with fast cleanup (100ms)
|
|
567
|
+
const fastStorage = new InMemoryRateLimitStorage(100);
|
|
568
|
+
const bucket = {
|
|
569
|
+
tokens: 10,
|
|
570
|
+
lastRefill: Date.now(),
|
|
571
|
+
firstRequest: Date.now(),
|
|
572
|
+
};
|
|
573
|
+
// Add multiple entries with short TTL
|
|
574
|
+
await fastStorage.set('key1', bucket, 50);
|
|
575
|
+
await fastStorage.set('key2', bucket, 50);
|
|
576
|
+
await fastStorage.set('key3', bucket, 50);
|
|
577
|
+
// Wait for cleanup
|
|
578
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
579
|
+
const stats = fastStorage.getStats();
|
|
580
|
+
expect(stats.size).toBe(0);
|
|
581
|
+
fastStorage.dispose();
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
describe('Statistics', () => {
|
|
585
|
+
it('should return accurate stats', async () => {
|
|
586
|
+
const bucket = {
|
|
587
|
+
tokens: 10,
|
|
588
|
+
lastRefill: Date.now(),
|
|
589
|
+
firstRequest: Date.now(),
|
|
590
|
+
};
|
|
591
|
+
await storage.set('key1', bucket, 60000);
|
|
592
|
+
await storage.set('key2', bucket, 60000);
|
|
593
|
+
const stats = storage.getStats();
|
|
594
|
+
expect(stats.size).toBe(2);
|
|
595
|
+
expect(stats.keys).toContain('key1');
|
|
596
|
+
expect(stats.keys).toContain('key2');
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
describe('Rate Limiting Scenarios', () => {
|
|
601
|
+
let storage;
|
|
602
|
+
beforeEach(() => {
|
|
603
|
+
storage = new InMemoryRateLimitStorage();
|
|
604
|
+
});
|
|
605
|
+
afterEach(() => {
|
|
606
|
+
storage.dispose();
|
|
607
|
+
});
|
|
608
|
+
describe('API Endpoint Rate Limiting', () => {
|
|
609
|
+
it('should limit search API requests per IP', async () => {
|
|
610
|
+
const limiter = new RateLimiter(RATE_LIMIT_PRESETS.STANDARD, storage);
|
|
611
|
+
const ip = '192.168.1.1';
|
|
612
|
+
// Simulate 30 search requests (at limit)
|
|
613
|
+
for (let i = 0; i < 30; i++) {
|
|
614
|
+
const result = await limiter.checkLimit(`ip:${ip}`);
|
|
615
|
+
expect(result.allowed).toBe(true);
|
|
616
|
+
}
|
|
617
|
+
// 31st request should be denied
|
|
618
|
+
const result = await limiter.checkLimit(`ip:${ip}`);
|
|
619
|
+
expect(result.allowed).toBe(false);
|
|
620
|
+
expect(result.retryAfterMs).toBeGreaterThan(0);
|
|
621
|
+
});
|
|
622
|
+
it('should limit install operations per user', async () => {
|
|
623
|
+
// Stricter limits for install operations
|
|
624
|
+
const limiter = new RateLimiter({
|
|
625
|
+
maxTokens: 5,
|
|
626
|
+
refillRate: 5 / 60, // 5 per minute
|
|
627
|
+
windowMs: 60000,
|
|
628
|
+
}, storage);
|
|
629
|
+
const userId = 'user-123';
|
|
630
|
+
// User can install 5 skills
|
|
631
|
+
for (let i = 0; i < 5; i++) {
|
|
632
|
+
const result = await limiter.checkLimit(`user:${userId}:install`);
|
|
633
|
+
expect(result.allowed).toBe(true);
|
|
634
|
+
}
|
|
635
|
+
// 6th install should be denied
|
|
636
|
+
const result = await limiter.checkLimit(`user:${userId}:install`);
|
|
637
|
+
expect(result.allowed).toBe(false);
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
describe('Source Adapter Rate Limiting', () => {
|
|
641
|
+
it('should limit GitHub API requests', async () => {
|
|
642
|
+
const limiter = new RateLimiter({
|
|
643
|
+
maxTokens: 60,
|
|
644
|
+
refillRate: 1, // 60 per minute
|
|
645
|
+
windowMs: 60000,
|
|
646
|
+
}, storage);
|
|
647
|
+
// Simulate batch of API calls
|
|
648
|
+
for (let i = 0; i < 60; i++) {
|
|
649
|
+
const result = await limiter.checkLimit('adapter:github');
|
|
650
|
+
expect(result.allowed).toBe(true);
|
|
651
|
+
}
|
|
652
|
+
// Next call should be rate limited
|
|
653
|
+
const result = await limiter.checkLimit('adapter:github');
|
|
654
|
+
expect(result.allowed).toBe(false);
|
|
655
|
+
});
|
|
656
|
+
it('should limit raw URL fetches', async () => {
|
|
657
|
+
const limiter = new RateLimiter(RATE_LIMIT_PRESETS.STANDARD, storage);
|
|
658
|
+
// Simulate URL fetches
|
|
659
|
+
for (let i = 0; i < 30; i++) {
|
|
660
|
+
const result = await limiter.checkLimit('adapter:raw-url');
|
|
661
|
+
expect(result.allowed).toBe(true);
|
|
662
|
+
}
|
|
663
|
+
const result = await limiter.checkLimit('adapter:raw-url');
|
|
664
|
+
expect(result.allowed).toBe(false);
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
describe('Burst Traffic', () => {
|
|
668
|
+
it('should allow burst within token capacity', async () => {
|
|
669
|
+
// Use fake timers to prevent token refill during parallel execution
|
|
670
|
+
vi.useFakeTimers();
|
|
671
|
+
const limiter = new RateLimiter({
|
|
672
|
+
maxTokens: 20,
|
|
673
|
+
refillRate: 5, // Refill slowly
|
|
674
|
+
windowMs: 60000,
|
|
675
|
+
}, storage);
|
|
676
|
+
// Burst of 20 requests - should all succeed
|
|
677
|
+
// Use sequential execution to ensure deterministic token consumption
|
|
678
|
+
for (let i = 0; i < 20; i++) {
|
|
679
|
+
const result = await limiter.checkLimit('test-key');
|
|
680
|
+
expect(result.allowed).toBe(true);
|
|
681
|
+
}
|
|
682
|
+
// 21st should fail (all tokens consumed)
|
|
683
|
+
const result = await limiter.checkLimit('test-key');
|
|
684
|
+
expect(result.allowed).toBe(false);
|
|
685
|
+
vi.useRealTimers();
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
/**
|
|
690
|
+
* Queue functionality tests - SMI-1013
|
|
691
|
+
*/
|
|
692
|
+
describe('RateLimiter - Queue Support (SMI-1013)', () => {
|
|
693
|
+
let limiter;
|
|
694
|
+
let storage;
|
|
695
|
+
beforeEach(() => {
|
|
696
|
+
storage = new InMemoryRateLimitStorage();
|
|
697
|
+
});
|
|
698
|
+
afterEach(() => {
|
|
699
|
+
storage.dispose();
|
|
700
|
+
if (limiter) {
|
|
701
|
+
limiter.dispose();
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
describe('waitForToken()', () => {
|
|
705
|
+
it('should return immediately when tokens are available', async () => {
|
|
706
|
+
limiter = new RateLimiter({
|
|
707
|
+
maxTokens: 10,
|
|
708
|
+
refillRate: 1,
|
|
709
|
+
windowMs: 60000,
|
|
710
|
+
enableQueue: true,
|
|
711
|
+
queueTimeoutMs: 5000,
|
|
712
|
+
}, storage);
|
|
713
|
+
const result = await limiter.waitForToken('test-key');
|
|
714
|
+
expect(result.allowed).toBe(true);
|
|
715
|
+
expect(result.queued).toBe(false);
|
|
716
|
+
expect(result.remaining).toBe(9);
|
|
717
|
+
});
|
|
718
|
+
it('should queue request when no tokens available', async () => {
|
|
719
|
+
limiter = new RateLimiter({
|
|
720
|
+
maxTokens: 2,
|
|
721
|
+
refillRate: 10, // Fast refill: 10 tokens/sec
|
|
722
|
+
windowMs: 60000,
|
|
723
|
+
enableQueue: true,
|
|
724
|
+
queueTimeoutMs: 5000,
|
|
725
|
+
}, storage);
|
|
726
|
+
// Use all tokens
|
|
727
|
+
await limiter.waitForToken('test-key');
|
|
728
|
+
await limiter.waitForToken('test-key');
|
|
729
|
+
// Third request should be queued and wait for refill
|
|
730
|
+
const startTime = Date.now();
|
|
731
|
+
const result = await limiter.waitForToken('test-key');
|
|
732
|
+
const elapsed = Date.now() - startTime;
|
|
733
|
+
expect(result.allowed).toBe(true);
|
|
734
|
+
expect(result.queued).toBe(true);
|
|
735
|
+
expect(result.queueWaitMs).toBeGreaterThan(0);
|
|
736
|
+
expect(elapsed).toBeGreaterThan(50); // Should have waited for tokens
|
|
737
|
+
});
|
|
738
|
+
it('should respect configured limit with sequential requests', async () => {
|
|
739
|
+
limiter = new RateLimiter({
|
|
740
|
+
maxTokens: 3,
|
|
741
|
+
refillRate: 10, // Fast refill for quick test
|
|
742
|
+
windowMs: 60000,
|
|
743
|
+
enableQueue: true,
|
|
744
|
+
queueTimeoutMs: 5000,
|
|
745
|
+
}, storage);
|
|
746
|
+
// Make 3 requests - should all succeed immediately
|
|
747
|
+
const r1 = await limiter.waitForToken('test-key');
|
|
748
|
+
const r2 = await limiter.waitForToken('test-key');
|
|
749
|
+
const r3 = await limiter.waitForToken('test-key');
|
|
750
|
+
expect(r1.allowed).toBe(true);
|
|
751
|
+
expect(r2.allowed).toBe(true);
|
|
752
|
+
expect(r3.allowed).toBe(true);
|
|
753
|
+
// These should not have been queued (immediate success)
|
|
754
|
+
expect(r1.queued).toBe(false);
|
|
755
|
+
expect(r2.queued).toBe(false);
|
|
756
|
+
expect(r3.queued).toBe(false);
|
|
757
|
+
});
|
|
758
|
+
it('should timeout when queue wait exceeds limit', async () => {
|
|
759
|
+
limiter = new RateLimiter({
|
|
760
|
+
maxTokens: 1,
|
|
761
|
+
refillRate: 0.01, // Very slow: 0.01 tokens/sec (100 seconds per token)
|
|
762
|
+
windowMs: 60000,
|
|
763
|
+
enableQueue: true,
|
|
764
|
+
queueTimeoutMs: 100, // 100ms timeout
|
|
765
|
+
}, storage);
|
|
766
|
+
// Use the only token
|
|
767
|
+
await limiter.waitForToken('test-key');
|
|
768
|
+
// Next request should timeout
|
|
769
|
+
await expect(limiter.waitForToken('test-key')).rejects.toThrow(RateLimitQueueTimeoutError);
|
|
770
|
+
});
|
|
771
|
+
it('should throw RateLimitQueueFullError when queue is at capacity', async () => {
|
|
772
|
+
limiter = new RateLimiter({
|
|
773
|
+
maxTokens: 1,
|
|
774
|
+
refillRate: 0.01, // Very slow
|
|
775
|
+
windowMs: 60000,
|
|
776
|
+
enableQueue: true,
|
|
777
|
+
queueTimeoutMs: 60000,
|
|
778
|
+
maxQueueSize: 2,
|
|
779
|
+
}, storage);
|
|
780
|
+
// Use the only token
|
|
781
|
+
await limiter.waitForToken('test-key');
|
|
782
|
+
// Queue 2 requests (max queue size) - add catch to prevent unhandled rejections
|
|
783
|
+
const p1 = limiter.waitForToken('test-key').catch(() => { });
|
|
784
|
+
const p2 = limiter.waitForToken('test-key').catch(() => { });
|
|
785
|
+
// Third queued request should fail immediately
|
|
786
|
+
await expect(limiter.waitForToken('test-key')).rejects.toThrow(RateLimitQueueFullError);
|
|
787
|
+
// Cleanup happens in afterEach
|
|
788
|
+
});
|
|
789
|
+
it('should fall back to checkLimit when queue is disabled', async () => {
|
|
790
|
+
limiter = new RateLimiter({
|
|
791
|
+
maxTokens: 2,
|
|
792
|
+
refillRate: 0.01,
|
|
793
|
+
windowMs: 60000,
|
|
794
|
+
enableQueue: false, // Queue disabled
|
|
795
|
+
}, storage);
|
|
796
|
+
// Use all tokens
|
|
797
|
+
await limiter.waitForToken('test-key');
|
|
798
|
+
await limiter.waitForToken('test-key');
|
|
799
|
+
// Third request should be denied immediately (no queuing)
|
|
800
|
+
const result = await limiter.waitForToken('test-key');
|
|
801
|
+
expect(result.allowed).toBe(false);
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
describe('getQueueStatus()', () => {
|
|
805
|
+
it('should return 0 for empty queue', async () => {
|
|
806
|
+
limiter = new RateLimiter({
|
|
807
|
+
maxTokens: 10,
|
|
808
|
+
refillRate: 1,
|
|
809
|
+
windowMs: 60000,
|
|
810
|
+
enableQueue: true,
|
|
811
|
+
queueTimeoutMs: 60000,
|
|
812
|
+
}, storage);
|
|
813
|
+
// No requests queued
|
|
814
|
+
const status = limiter.getQueueStatus('test-key');
|
|
815
|
+
expect(status).toBe(0);
|
|
816
|
+
});
|
|
817
|
+
it('should return all queue info when no key specified', async () => {
|
|
818
|
+
limiter = new RateLimiter({
|
|
819
|
+
maxTokens: 10,
|
|
820
|
+
refillRate: 1,
|
|
821
|
+
windowMs: 60000,
|
|
822
|
+
enableQueue: true,
|
|
823
|
+
queueTimeoutMs: 60000,
|
|
824
|
+
}, storage);
|
|
825
|
+
// No queued requests yet
|
|
826
|
+
const status = limiter.getQueueStatus();
|
|
827
|
+
expect(status.totalQueued).toBe(0);
|
|
828
|
+
expect(status.queues.size).toBe(0);
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
describe('clearQueue()', () => {
|
|
832
|
+
it('should not throw when clearing empty queue', () => {
|
|
833
|
+
limiter = new RateLimiter({
|
|
834
|
+
maxTokens: 10,
|
|
835
|
+
refillRate: 1,
|
|
836
|
+
windowMs: 60000,
|
|
837
|
+
enableQueue: true,
|
|
838
|
+
queueTimeoutMs: 60000,
|
|
839
|
+
}, storage);
|
|
840
|
+
// Clearing empty queue should not throw
|
|
841
|
+
expect(() => limiter.clearQueue('nonexistent-key')).not.toThrow();
|
|
842
|
+
expect(() => limiter.clearQueue()).not.toThrow();
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
describe('Queue with metrics', () => {
|
|
846
|
+
it('should track metrics for allowed requests with queue enabled', async () => {
|
|
847
|
+
limiter = new RateLimiter({
|
|
848
|
+
maxTokens: 5,
|
|
849
|
+
refillRate: 1,
|
|
850
|
+
windowMs: 60000,
|
|
851
|
+
enableQueue: true,
|
|
852
|
+
queueTimeoutMs: 5000,
|
|
853
|
+
}, storage);
|
|
854
|
+
// Make requests that succeed immediately
|
|
855
|
+
await limiter.waitForToken('test-key');
|
|
856
|
+
await limiter.waitForToken('test-key');
|
|
857
|
+
const metrics = limiter.getMetrics('test-key');
|
|
858
|
+
expect(metrics).toBeDefined();
|
|
859
|
+
expect(metrics.allowed).toBe(2);
|
|
860
|
+
expect(metrics.blocked).toBe(0);
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
describe('dispose()', () => {
|
|
864
|
+
it('should stop queue processor interval on dispose', () => {
|
|
865
|
+
const localLimiter = new RateLimiter({
|
|
866
|
+
maxTokens: 10,
|
|
867
|
+
refillRate: 1,
|
|
868
|
+
windowMs: 60000,
|
|
869
|
+
enableQueue: true,
|
|
870
|
+
}, storage);
|
|
871
|
+
// Dispose should not throw
|
|
872
|
+
expect(() => localLimiter.dispose()).not.toThrow();
|
|
873
|
+
});
|
|
874
|
+
it('should clean up resources without queue enabled', () => {
|
|
875
|
+
const localLimiter = new RateLimiter({
|
|
876
|
+
maxTokens: 10,
|
|
877
|
+
refillRate: 1,
|
|
878
|
+
windowMs: 60000,
|
|
879
|
+
enableQueue: false,
|
|
880
|
+
}, storage);
|
|
881
|
+
// Dispose should not throw
|
|
882
|
+
expect(() => localLimiter.dispose()).not.toThrow();
|
|
883
|
+
});
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
/**
|
|
887
|
+
* Memory bounds and cleanup tests - Code Review Wave 3
|
|
888
|
+
*/
|
|
889
|
+
describe('RateLimiter - Memory Bounds (Wave 3 Fixes)', () => {
|
|
890
|
+
let limiter;
|
|
891
|
+
let storage;
|
|
892
|
+
beforeEach(() => {
|
|
893
|
+
storage = new InMemoryRateLimitStorage();
|
|
894
|
+
});
|
|
895
|
+
afterEach(() => {
|
|
896
|
+
storage.dispose();
|
|
897
|
+
if (limiter) {
|
|
898
|
+
limiter.dispose();
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
describe('Metrics Memory Bounds', () => {
|
|
902
|
+
it('should track lastUpdated in metrics', async () => {
|
|
903
|
+
limiter = new RateLimiter({
|
|
904
|
+
maxTokens: 10,
|
|
905
|
+
refillRate: 1,
|
|
906
|
+
windowMs: 60000,
|
|
907
|
+
}, storage);
|
|
908
|
+
await limiter.checkLimit('test-key');
|
|
909
|
+
const metrics = limiter.getMetrics('test-key');
|
|
910
|
+
expect(metrics.lastUpdated).toBeInstanceOf(Date);
|
|
911
|
+
expect(metrics.lastUpdated.getTime()).toBeGreaterThan(0);
|
|
912
|
+
});
|
|
913
|
+
it('should evict oldest metrics when max unique keys exceeded', async () => {
|
|
914
|
+
// Create limiter and manually test eviction logic by checking metrics behavior
|
|
915
|
+
limiter = new RateLimiter({
|
|
916
|
+
maxTokens: 10,
|
|
917
|
+
refillRate: 1,
|
|
918
|
+
windowMs: 60000,
|
|
919
|
+
}, storage);
|
|
920
|
+
// Make requests for multiple keys
|
|
921
|
+
for (let i = 0; i < 100; i++) {
|
|
922
|
+
await limiter.checkLimit(`key-${i}`);
|
|
923
|
+
}
|
|
924
|
+
// Should have all 100 keys (under the 10000 limit)
|
|
925
|
+
const allMetrics = limiter.getMetrics();
|
|
926
|
+
expect(allMetrics.size).toBe(100);
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
describe('Queue Memory Bounds', () => {
|
|
930
|
+
it('should reject new queues when max unique keys for queues is reached', async () => {
|
|
931
|
+
limiter = new RateLimiter({
|
|
932
|
+
maxTokens: 1,
|
|
933
|
+
refillRate: 0.001, // Very slow
|
|
934
|
+
windowMs: 60000,
|
|
935
|
+
enableQueue: true,
|
|
936
|
+
queueTimeoutMs: 60000,
|
|
937
|
+
maxQueueSize: 1,
|
|
938
|
+
}, storage);
|
|
939
|
+
// Use token for first key
|
|
940
|
+
await limiter.waitForToken('key-1');
|
|
941
|
+
// Queue one request for key-1
|
|
942
|
+
const p1 = limiter.waitForToken('key-1').catch(() => { });
|
|
943
|
+
// Trying to add another request should fail (queue full for this key)
|
|
944
|
+
await expect(limiter.waitForToken('key-1')).rejects.toThrow(RateLimitQueueFullError);
|
|
945
|
+
// Cleanup
|
|
946
|
+
limiter.clearQueue();
|
|
947
|
+
});
|
|
948
|
+
it('should clean up empty queues during processing', async () => {
|
|
949
|
+
limiter = new RateLimiter({
|
|
950
|
+
maxTokens: 10,
|
|
951
|
+
refillRate: 10,
|
|
952
|
+
windowMs: 60000,
|
|
953
|
+
enableQueue: true,
|
|
954
|
+
queueTimeoutMs: 5000,
|
|
955
|
+
}, storage);
|
|
956
|
+
// Make immediate requests (not queued)
|
|
957
|
+
await limiter.waitForToken('key-1');
|
|
958
|
+
await limiter.waitForToken('key-2');
|
|
959
|
+
// Queue status should not have empty queues cluttering memory
|
|
960
|
+
const status = limiter.getQueueStatus();
|
|
961
|
+
expect(status.totalQueued).toBe(0);
|
|
962
|
+
});
|
|
963
|
+
});
|
|
964
|
+
describe('Queue Request ID Uniqueness', () => {
|
|
965
|
+
it('should use unique IDs for queued requests', async () => {
|
|
966
|
+
limiter = new RateLimiter({
|
|
967
|
+
maxTokens: 1,
|
|
968
|
+
refillRate: 10, // Fast refill
|
|
969
|
+
windowMs: 60000,
|
|
970
|
+
enableQueue: true,
|
|
971
|
+
queueTimeoutMs: 5000,
|
|
972
|
+
maxQueueSize: 10,
|
|
973
|
+
}, storage);
|
|
974
|
+
// Use the only token
|
|
975
|
+
await limiter.waitForToken('test-key');
|
|
976
|
+
// Queue multiple requests rapidly
|
|
977
|
+
const promises = [
|
|
978
|
+
limiter.waitForToken('test-key'),
|
|
979
|
+
limiter.waitForToken('test-key'),
|
|
980
|
+
limiter.waitForToken('test-key'),
|
|
981
|
+
];
|
|
982
|
+
// All should eventually complete (no ID collisions)
|
|
983
|
+
const results = await Promise.all(promises);
|
|
984
|
+
results.forEach((r) => {
|
|
985
|
+
expect(r.allowed).toBe(true);
|
|
986
|
+
expect(r.queued).toBe(true);
|
|
987
|
+
});
|
|
988
|
+
});
|
|
989
|
+
});
|
|
990
|
+
describe('Concurrent Queue Processing Safety', () => {
|
|
991
|
+
it('should handle concurrent queue operations safely', async () => {
|
|
992
|
+
limiter = new RateLimiter({
|
|
993
|
+
maxTokens: 2,
|
|
994
|
+
refillRate: 20, // Fast refill for test
|
|
995
|
+
windowMs: 60000,
|
|
996
|
+
enableQueue: true,
|
|
997
|
+
queueTimeoutMs: 5000,
|
|
998
|
+
maxQueueSize: 20,
|
|
999
|
+
}, storage);
|
|
1000
|
+
// Use tokens
|
|
1001
|
+
await limiter.waitForToken('test-key');
|
|
1002
|
+
await limiter.waitForToken('test-key');
|
|
1003
|
+
// Queue many concurrent requests
|
|
1004
|
+
const promises = [];
|
|
1005
|
+
for (let i = 0; i < 10; i++) {
|
|
1006
|
+
promises.push(limiter.waitForToken('test-key'));
|
|
1007
|
+
}
|
|
1008
|
+
// All should complete without errors
|
|
1009
|
+
const results = await Promise.all(promises);
|
|
1010
|
+
expect(results.length).toBe(10);
|
|
1011
|
+
results.forEach((r) => {
|
|
1012
|
+
expect(r.allowed).toBe(true);
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
//# sourceMappingURL=RateLimiter.test.js.map
|