bunsane 0.1.4 → 0.2.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/.claude/settings.local.json +47 -0
- package/.claude/skills/update-memory.md +74 -0
- package/.prettierrc +4 -0
- package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
- package/.serena/memories/architecture.md +154 -0
- package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
- package/.serena/memories/code_style_and_conventions.md +76 -0
- package/.serena/memories/project_overview.md +43 -0
- package/.serena/memories/schema-dsl-plan.md +107 -0
- package/.serena/memories/suggested_commands.md +80 -0
- package/.serena/memories/typescript-compilation-status.md +54 -0
- package/.serena/project.yml +114 -0
- package/TODO.md +1 -7
- package/bun.lock +150 -4
- package/bunfig.toml +10 -0
- package/config/cache.config.ts +77 -0
- package/config/upload.config.ts +4 -5
- package/core/App.ts +870 -123
- package/core/ArcheType.ts +2268 -377
- package/core/BatchLoader.ts +181 -71
- package/core/Config.ts +153 -0
- package/core/Decorators.ts +4 -1
- package/core/Entity.ts +621 -92
- package/core/EntityHookManager.ts +1 -1
- package/core/EntityInterface.ts +3 -1
- package/core/EntityManager.ts +1 -13
- package/core/ErrorHandler.ts +8 -2
- package/core/Logger.ts +9 -0
- package/core/Middleware.ts +34 -0
- package/core/RequestContext.ts +5 -1
- package/core/RequestLoaders.ts +227 -93
- package/core/SchedulerManager.ts +193 -52
- package/core/cache/CacheAnalytics.ts +399 -0
- package/core/cache/CacheFactory.ts +145 -0
- package/core/cache/CacheManager.ts +520 -0
- package/core/cache/CacheProvider.ts +34 -0
- package/core/cache/CacheWarmer.ts +157 -0
- package/core/cache/CompressionUtils.ts +110 -0
- package/core/cache/MemoryCache.ts +251 -0
- package/core/cache/MultiLevelCache.ts +180 -0
- package/core/cache/NoOpCache.ts +53 -0
- package/core/cache/RedisCache.ts +464 -0
- package/core/cache/TTLStrategy.ts +254 -0
- package/core/cache/index.ts +6 -0
- package/core/components/BaseComponent.ts +120 -0
- package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
- package/core/components/Decorators.ts +88 -0
- package/core/components/Interfaces.ts +7 -0
- package/core/components/index.ts +5 -0
- package/core/decorators/EntityHooks.ts +0 -3
- package/core/decorators/IndexedField.ts +26 -0
- package/core/decorators/ScheduledTask.ts +0 -47
- package/core/events/EntityLifecycleEvents.ts +1 -1
- package/core/health.ts +112 -0
- package/core/metadata/definitions/ArcheType.ts +14 -0
- package/core/metadata/definitions/Component.ts +9 -0
- package/core/metadata/definitions/gqlObject.ts +1 -1
- package/core/metadata/index.ts +42 -1
- package/core/metadata/metadata-storage.ts +28 -2
- package/core/middleware/AccessLog.ts +59 -0
- package/core/middleware/RequestId.ts +38 -0
- package/core/middleware/SecurityHeaders.ts +62 -0
- package/core/middleware/index.ts +3 -0
- package/core/scheduler/DistributedLock.ts +266 -0
- package/core/scheduler/index.ts +15 -0
- package/core/validateEnv.ts +92 -0
- package/database/DatabaseHelper.ts +416 -40
- package/database/IndexingStrategy.ts +342 -0
- package/database/PreparedStatementCache.ts +226 -0
- package/database/index.ts +32 -7
- package/database/sqlHelpers.ts +14 -2
- package/endpoints/archetypes.ts +362 -0
- package/endpoints/components.ts +58 -0
- package/endpoints/entity.ts +80 -0
- package/endpoints/index.ts +27 -0
- package/endpoints/query.ts +93 -0
- package/endpoints/stats.ts +76 -0
- package/endpoints/tables.ts +212 -0
- package/endpoints/types.ts +155 -0
- package/gql/ArchetypeOperations.ts +32 -86
- package/gql/Generator.ts +27 -315
- package/gql/GeneratorV2.ts +37 -0
- package/gql/builders/InputTypeBuilder.ts +99 -0
- package/gql/builders/ResolverBuilder.ts +234 -0
- package/gql/builders/TypeDefBuilder.ts +105 -0
- package/gql/builders/index.ts +3 -0
- package/gql/decorators/Upload.ts +1 -1
- package/gql/depthLimit.ts +85 -0
- package/gql/graph/GraphNode.ts +224 -0
- package/gql/graph/SchemaGraph.ts +278 -0
- package/gql/helpers.ts +8 -2
- package/gql/index.ts +56 -4
- package/gql/middleware.ts +79 -0
- package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
- package/gql/orchestration/index.ts +1 -0
- package/gql/scanner/ServiceScanner.ts +347 -0
- package/gql/schema/index.ts +458 -0
- package/gql/strategies/TypeGenerationStrategy.ts +329 -0
- package/gql/types.ts +1 -0
- package/gql/utils/TypeSignature.ts +220 -0
- package/gql/utils/index.ts +1 -0
- package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
- package/gql/visitors/DeduplicationVisitor.ts +82 -0
- package/gql/visitors/GraphVisitor.ts +78 -0
- package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
- package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
- package/gql/visitors/TypeCollectorVisitor.ts +79 -0
- package/gql/visitors/VisitorComposer.ts +96 -0
- package/gql/visitors/index.ts +7 -0
- package/package.json +59 -37
- package/plugins/index.ts +2 -2
- package/query/CTENode.ts +97 -0
- package/query/ComponentInclusionNode.ts +689 -0
- package/query/FilterBuilder.ts +127 -0
- package/query/FilterBuilderRegistry.ts +202 -0
- package/query/OrNode.ts +517 -0
- package/query/OrQuery.ts +42 -0
- package/query/Query.ts +1022 -0
- package/query/QueryContext.ts +170 -0
- package/query/QueryDAG.ts +122 -0
- package/query/QueryNode.ts +65 -0
- package/query/SourceNode.ts +53 -0
- package/query/builders/FullTextSearchBuilder.ts +236 -0
- package/query/index.ts +21 -0
- package/scheduler/index.ts +40 -8
- package/service/Service.ts +2 -1
- package/service/ServiceRegistry.ts +6 -5
- package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
- package/storage/S3StorageProvider.ts +316 -0
- package/{core/storage → storage}/StorageProvider.ts +7 -3
- package/studio/bun.lock +482 -0
- package/studio/index.html +13 -0
- package/studio/package.json +39 -0
- package/studio/postcss.config.js +6 -0
- package/studio/src/components/DataTable.tsx +211 -0
- package/studio/src/components/Layout.tsx +13 -0
- package/studio/src/components/PageContainer.tsx +9 -0
- package/studio/src/components/PageHeader.tsx +13 -0
- package/studio/src/components/SearchBar.tsx +57 -0
- package/studio/src/components/Sidebar.tsx +294 -0
- package/studio/src/components/ui/button.tsx +56 -0
- package/studio/src/components/ui/checkbox.tsx +26 -0
- package/studio/src/components/ui/input.tsx +25 -0
- package/studio/src/hooks/useDataTable.ts +131 -0
- package/studio/src/index.css +36 -0
- package/studio/src/lib/api.ts +186 -0
- package/studio/src/lib/utils.ts +13 -0
- package/studio/src/main.tsx +17 -0
- package/studio/src/pages/ArcheType.tsx +239 -0
- package/studio/src/pages/Components.tsx +124 -0
- package/studio/src/pages/EntityInspector.tsx +302 -0
- package/studio/src/pages/QueryRunner.tsx +246 -0
- package/studio/src/pages/Table.tsx +94 -0
- package/studio/src/pages/Welcome.tsx +241 -0
- package/studio/src/routes.tsx +45 -0
- package/studio/src/store/archeTypeSettings.ts +30 -0
- package/studio/src/store/studio.ts +65 -0
- package/studio/src/utils/columnHelpers.tsx +114 -0
- package/studio/studio-instructions.md +81 -0
- package/studio/tailwind.config.js +77 -0
- package/studio/tsconfig.json +24 -0
- package/studio/utils.ts +54 -0
- package/studio/vite.config.js +19 -0
- package/swagger/generator.ts +1 -1
- package/tests/e2e/http.test.ts +126 -0
- package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
- package/tests/fixtures/components/TestOrder.ts +23 -0
- package/tests/fixtures/components/TestProduct.ts +23 -0
- package/tests/fixtures/components/TestUser.ts +20 -0
- package/tests/fixtures/components/index.ts +6 -0
- package/tests/graphql/SchemaGeneration.test.ts +90 -0
- package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
- package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
- package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
- package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
- package/tests/integration/entity/Entity.persistence.test.ts +333 -0
- package/tests/integration/query/Query.exec.test.ts +523 -0
- package/tests/pglite-setup.ts +61 -0
- package/tests/setup.ts +164 -0
- package/tests/stress/BenchmarkRunner.ts +203 -0
- package/tests/stress/DataSeeder.ts +190 -0
- package/tests/stress/StressTestReporter.ts +229 -0
- package/tests/stress/cursor-perf-test.ts +171 -0
- package/tests/stress/fixtures/StressTestComponents.ts +58 -0
- package/tests/stress/index.ts +7 -0
- package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
- package/tests/unit/BatchLoader.test.ts +82 -0
- package/tests/unit/archetype/ArcheType.test.ts +107 -0
- package/tests/unit/cache/CacheManager.test.ts +347 -0
- package/tests/unit/cache/MemoryCache.test.ts +260 -0
- package/tests/unit/cache/RedisCache.test.ts +411 -0
- package/tests/unit/entity/Entity.components.test.ts +244 -0
- package/tests/unit/entity/Entity.test.ts +345 -0
- package/tests/unit/gql/depthLimit.test.ts +203 -0
- package/tests/unit/gql/operationMiddleware.test.ts +293 -0
- package/tests/unit/health/Health.test.ts +129 -0
- package/tests/unit/middleware/AccessLog.test.ts +37 -0
- package/tests/unit/middleware/Middleware.test.ts +98 -0
- package/tests/unit/middleware/RequestId.test.ts +54 -0
- package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
- package/tests/unit/query/FilterBuilder.test.ts +111 -0
- package/tests/unit/query/Query.test.ts +308 -0
- package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
- package/tests/unit/schema/schema-integration.test.ts +426 -0
- package/tests/unit/schema/schema.test.ts +580 -0
- package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
- package/tests/unit/upload/RestUpload.test.ts +267 -0
- package/tests/unit/validateEnv.test.ts +82 -0
- package/tests/utils/entity-tracker.ts +57 -0
- package/tests/utils/index.ts +13 -0
- package/tests/utils/test-context.ts +149 -0
- package/tsconfig.json +5 -1
- package/types/archetype.types.ts +6 -0
- package/types/hooks.types.ts +1 -1
- package/types/query.types.ts +110 -0
- package/types/scheduler.types.ts +68 -7
- package/types/upload.types.ts +1 -0
- package/{core → upload}/FileValidator.ts +10 -1
- package/upload/RestUpload.ts +130 -0
- package/{core/components → upload}/UploadComponent.ts +11 -11
- package/{core → upload}/UploadManager.ts +3 -3
- package/upload/index.ts +23 -7
- package/utils/UploadHelper.ts +27 -6
- package/utils/cronParser.ts +16 -6
- package/.github/workflows/deploy-docs.yml +0 -57
- package/core/Components.ts +0 -202
- package/core/EntityCache.ts +0 -15
- package/core/Query.ts +0 -880
- package/docs/README.md +0 -149
- package/docs/_coverpage.md +0 -36
- package/docs/_sidebar.md +0 -23
- package/docs/api/core.md +0 -568
- package/docs/api/hooks.md +0 -554
- package/docs/api/index.md +0 -222
- package/docs/api/query.md +0 -678
- package/docs/api/service.md +0 -744
- package/docs/core-concepts/archetypes.md +0 -512
- package/docs/core-concepts/components.md +0 -498
- package/docs/core-concepts/entity.md +0 -314
- package/docs/core-concepts/hooks.md +0 -683
- package/docs/core-concepts/query.md +0 -588
- package/docs/core-concepts/services.md +0 -647
- package/docs/examples/code-examples.md +0 -425
- package/docs/getting-started.md +0 -337
- package/docs/index.html +0 -97
- package/tests/bench/insert.bench.ts +0 -60
- package/tests/bench/relations.bench.ts +0 -270
- package/tests/bench/sorting.bench.ts +0 -416
- package/tests/component-hooks-simple.test.ts +0 -117
- package/tests/component-hooks.test.ts +0 -1461
- package/tests/component.test.ts +0 -339
- package/tests/errorHandling.test.ts +0 -155
- package/tests/hooks.test.ts +0 -667
- package/tests/query-sorting.test.ts +0 -101
- package/tests/query.test.ts +0 -81
- package/tests/relations.test.ts +0 -170
- package/tests/scheduler.test.ts +0 -724
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { CacheManager } from './CacheManager.js';
|
|
2
|
+
import { SchedulerManager } from '../SchedulerManager.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CacheWarmer preloads frequently accessed data into the cache to improve
|
|
6
|
+
* application startup performance and reduce initial request latency.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Preloading of frequently accessed entities
|
|
10
|
+
* - Scheduled cache warming with cron support
|
|
11
|
+
* - Configurable warming strategies
|
|
12
|
+
* - Performance monitoring during warming
|
|
13
|
+
*/
|
|
14
|
+
export class CacheWarmer {
|
|
15
|
+
private cacheManager: CacheManager;
|
|
16
|
+
private scheduler: SchedulerManager;
|
|
17
|
+
private warmingJobs: Map<string, { cancel: () => void }> = new Map();
|
|
18
|
+
|
|
19
|
+
constructor(cacheManager: CacheManager, scheduler: SchedulerManager) {
|
|
20
|
+
this.cacheManager = cacheManager;
|
|
21
|
+
this.scheduler = scheduler;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Warms the cache by preloading frequently accessed entities
|
|
26
|
+
*/
|
|
27
|
+
async warmEntityCache(entityIds: string[], entityType: string): Promise<{
|
|
28
|
+
success: boolean;
|
|
29
|
+
warmed: number;
|
|
30
|
+
failed: number;
|
|
31
|
+
duration: number;
|
|
32
|
+
}> {
|
|
33
|
+
const startTime = Date.now();
|
|
34
|
+
let warmed = 0;
|
|
35
|
+
let failed = 0;
|
|
36
|
+
|
|
37
|
+
console.log(`Starting entity cache warming for ${entityIds.length} ${entityType} entities`);
|
|
38
|
+
|
|
39
|
+
// Process entities in batches to avoid overwhelming the database
|
|
40
|
+
const batchSize = 10;
|
|
41
|
+
for (let i = 0; i < entityIds.length; i += batchSize) {
|
|
42
|
+
const batch = entityIds.slice(i, i + batchSize);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Load entities (this will populate the cache via write-through strategy)
|
|
46
|
+
const entities = await this.loadEntitiesBatch(batch, entityType);
|
|
47
|
+
warmed += entities.length;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.warn(`Failed to warm batch of entities:`, error);
|
|
50
|
+
failed += batch.length;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Small delay between batches to prevent database overload
|
|
54
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const duration = Date.now() - startTime;
|
|
58
|
+
console.log(`Entity cache warming completed: ${warmed} warmed, ${failed} failed in ${duration}ms`);
|
|
59
|
+
|
|
60
|
+
return { success: failed === 0, warmed, failed, duration };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Schedules periodic cache warming
|
|
65
|
+
*/
|
|
66
|
+
scheduleWarming(config: {
|
|
67
|
+
name: string;
|
|
68
|
+
cronExpression: string;
|
|
69
|
+
type: 'entity';
|
|
70
|
+
config: { entityIds: string[]; entityType: string };
|
|
71
|
+
enabled?: boolean;
|
|
72
|
+
}): void {
|
|
73
|
+
if (!config.enabled) {
|
|
74
|
+
console.log(`Cache warming job "${config.name}" is disabled`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Cancel existing job if it exists
|
|
79
|
+
this.cancelWarming(config.name);
|
|
80
|
+
|
|
81
|
+
const job = this.scheduler.scheduleJob(config.name, config.cronExpression, async () => {
|
|
82
|
+
try {
|
|
83
|
+
console.log(`Running scheduled cache warming: ${config.name}`);
|
|
84
|
+
|
|
85
|
+
if (config.type === 'entity') {
|
|
86
|
+
await this.warmEntityCache(config.config.entityIds, config.config.entityType);
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error(`Scheduled cache warming failed for "${config.name}":`, error);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
this.warmingJobs.set(config.name, job);
|
|
94
|
+
console.log(`Scheduled cache warming job "${config.name}" with cron: ${config.cronExpression}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Cancels a scheduled warming job
|
|
99
|
+
*/
|
|
100
|
+
cancelWarming(name: string): boolean {
|
|
101
|
+
const job = this.warmingJobs.get(name);
|
|
102
|
+
if (job) {
|
|
103
|
+
job.cancel();
|
|
104
|
+
this.warmingJobs.delete(name);
|
|
105
|
+
console.log(`Cancelled cache warming job: ${name}`);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Gets list of active warming jobs
|
|
113
|
+
*/
|
|
114
|
+
getActiveJobs(): string[] {
|
|
115
|
+
return Array.from(this.warmingJobs.keys());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Performs a comprehensive cache warming operation
|
|
120
|
+
*/
|
|
121
|
+
async warmAll(config: {
|
|
122
|
+
entities?: Array<{ entityIds: string[]; entityType: string }>;
|
|
123
|
+
}): Promise<{
|
|
124
|
+
entities: { success: boolean; warmed: number; failed: number; duration: number };
|
|
125
|
+
totalDuration: number;
|
|
126
|
+
}> {
|
|
127
|
+
const startTime = Date.now();
|
|
128
|
+
|
|
129
|
+
// Warm entities
|
|
130
|
+
const firstEntity = config.entities?.[0];
|
|
131
|
+
const entityResults = firstEntity
|
|
132
|
+
? await this.warmEntityCache(firstEntity.entityIds, firstEntity.entityType)
|
|
133
|
+
: { success: true, warmed: 0, failed: 0, duration: 0 };
|
|
134
|
+
|
|
135
|
+
const totalDuration = Date.now() - startTime;
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
entities: entityResults,
|
|
139
|
+
totalDuration
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Loads a batch of entities (placeholder - would need actual entity loading logic)
|
|
145
|
+
*/
|
|
146
|
+
private async loadEntitiesBatch(entityIds: string[], entityType: string): Promise<any[]> {
|
|
147
|
+
// This is a placeholder - in a real implementation, this would load entities
|
|
148
|
+
// from the database using the appropriate entity manager or query system
|
|
149
|
+
console.log(`Loading batch of ${entityIds.length} ${entityType} entities: ${entityIds.slice(0, 3).join(', ')}...`);
|
|
150
|
+
|
|
151
|
+
// Simulate loading delay
|
|
152
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
153
|
+
|
|
154
|
+
// Return mock entities - in real implementation, this would be actual entity data
|
|
155
|
+
return entityIds.map(id => ({ id, type: entityType, loaded: true }));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { gzip, gunzip } from 'zlib';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
|
|
4
|
+
const gzipAsync = promisify(gzip);
|
|
5
|
+
const gunzipAsync = promisify(gunzip);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compression utilities for cache payloads. Automatically compresses data
|
|
9
|
+
* over 1KB threshold to reduce memory usage and network transfer for Redis.
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Gzip compression for payloads > 1KB
|
|
13
|
+
* - Automatic compression/decompression
|
|
14
|
+
* - Metadata tracking for compressed data
|
|
15
|
+
* - Error handling with fallback to uncompressed data
|
|
16
|
+
*/
|
|
17
|
+
export class CompressionUtils {
|
|
18
|
+
private static readonly COMPRESSION_THRESHOLD = 1024; // 1KB
|
|
19
|
+
private static readonly COMPRESSION_PREFIX = '__COMPRESSED__';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compresses data if it exceeds the threshold size
|
|
23
|
+
*/
|
|
24
|
+
static async compress(data: any): Promise<any> {
|
|
25
|
+
try {
|
|
26
|
+
const serialized = JSON.stringify(data);
|
|
27
|
+
const size = Buffer.byteLength(serialized, 'utf8');
|
|
28
|
+
|
|
29
|
+
if (size <= this.COMPRESSION_THRESHOLD) {
|
|
30
|
+
return data; // No compression needed
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const compressed = await gzipAsync(serialized);
|
|
34
|
+
const compressedData = compressed.toString('base64');
|
|
35
|
+
|
|
36
|
+
// Return compressed data with metadata
|
|
37
|
+
return {
|
|
38
|
+
[this.COMPRESSION_PREFIX]: true,
|
|
39
|
+
data: compressedData,
|
|
40
|
+
originalSize: size,
|
|
41
|
+
compressedSize: compressed.length
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Fallback to uncompressed data on compression error
|
|
45
|
+
console.warn('Compression failed, using uncompressed data:', error);
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Decompresses data if it was previously compressed
|
|
52
|
+
*/
|
|
53
|
+
static async decompress(data: any): Promise<any> {
|
|
54
|
+
try {
|
|
55
|
+
// Check if data is compressed
|
|
56
|
+
if (typeof data === 'object' && data !== null && data[this.COMPRESSION_PREFIX]) {
|
|
57
|
+
const compressedBuffer = Buffer.from(data.data, 'base64');
|
|
58
|
+
const decompressed = await gunzipAsync(compressedBuffer);
|
|
59
|
+
return JSON.parse(decompressed.toString('utf8'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Data is not compressed
|
|
63
|
+
return data;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// Fallback to original data on decompression error
|
|
66
|
+
console.warn('Decompression failed, using original data:', error);
|
|
67
|
+
return data;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Checks if data is compressed
|
|
73
|
+
*/
|
|
74
|
+
static isCompressed(data: any): boolean {
|
|
75
|
+
return typeof data === 'object' && data !== null && data[this.COMPRESSION_PREFIX] === true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Gets compression statistics for monitoring
|
|
80
|
+
*/
|
|
81
|
+
static getCompressionStats(data: any): { compressed: boolean; originalSize?: number; compressedSize?: number; ratio?: number } {
|
|
82
|
+
if (this.isCompressed(data)) {
|
|
83
|
+
const originalSize = data.originalSize;
|
|
84
|
+
const compressedSize = data.compressedSize;
|
|
85
|
+
const ratio = originalSize > 0 ? compressedSize / originalSize : 0;
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
compressed: true,
|
|
89
|
+
originalSize,
|
|
90
|
+
compressedSize,
|
|
91
|
+
ratio
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { compressed: false };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Estimates if data would benefit from compression
|
|
100
|
+
*/
|
|
101
|
+
static shouldCompress(data: any): boolean {
|
|
102
|
+
try {
|
|
103
|
+
const serialized = JSON.stringify(data);
|
|
104
|
+
const size = Buffer.byteLength(serialized, 'utf8');
|
|
105
|
+
return size > this.COMPRESSION_THRESHOLD;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { type CacheProvider, type CacheStats } from './CacheProvider';
|
|
2
|
+
import { logger } from '../Logger';
|
|
3
|
+
|
|
4
|
+
interface CacheEntry<T> {
|
|
5
|
+
value: T;
|
|
6
|
+
expiresAt?: number;
|
|
7
|
+
lastAccessed: number;
|
|
8
|
+
accessCount: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface MemoryCacheConfig {
|
|
12
|
+
maxSize?: number; // Maximum number of entries
|
|
13
|
+
maxMemory?: number; // Maximum memory usage in bytes
|
|
14
|
+
defaultTTL?: number; // Default TTL in milliseconds
|
|
15
|
+
cleanupInterval?: number; // Cleanup interval in milliseconds
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* In-memory cache implementation with TTL and LRU eviction
|
|
20
|
+
*/
|
|
21
|
+
export class MemoryCache implements CacheProvider {
|
|
22
|
+
private cache = new Map<string, CacheEntry<any>>();
|
|
23
|
+
private config: Required<MemoryCacheConfig>;
|
|
24
|
+
private cleanupTimer?: Timer;
|
|
25
|
+
private stats = {
|
|
26
|
+
hits: 0,
|
|
27
|
+
misses: 0,
|
|
28
|
+
size: 0,
|
|
29
|
+
memoryUsage: 0
|
|
30
|
+
};
|
|
31
|
+
private accessCounter = 0; // For LRU ordering
|
|
32
|
+
|
|
33
|
+
constructor(config: MemoryCacheConfig = {}) {
|
|
34
|
+
this.config = {
|
|
35
|
+
maxSize: config.maxSize ?? 10000,
|
|
36
|
+
maxMemory: config.maxMemory ?? 100 * 1024 * 1024, // 100MB default
|
|
37
|
+
defaultTTL: config.defaultTTL ?? 3600000, // 1 hour default
|
|
38
|
+
cleanupInterval: config.cleanupInterval ?? 60000 // 1 minute default
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
this.startCleanupTimer();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async get<T>(key: string): Promise<T | null> {
|
|
45
|
+
const entry = this.cache.get(key);
|
|
46
|
+
|
|
47
|
+
if (!entry) {
|
|
48
|
+
this.stats.misses++;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if expired
|
|
53
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
54
|
+
this.cache.delete(key);
|
|
55
|
+
this.stats.misses++;
|
|
56
|
+
this.updateMemoryUsage();
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Update access tracking for LRU
|
|
61
|
+
entry.lastAccessed = ++this.accessCounter;
|
|
62
|
+
entry.accessCount++;
|
|
63
|
+
|
|
64
|
+
this.stats.hits++;
|
|
65
|
+
return entry.value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
|
|
69
|
+
const expiresAt = ttl ? Date.now() + ttl : (this.config.defaultTTL ? Date.now() + this.config.defaultTTL : undefined);
|
|
70
|
+
|
|
71
|
+
const entry: CacheEntry<T> = {
|
|
72
|
+
value,
|
|
73
|
+
expiresAt,
|
|
74
|
+
lastAccessed: ++this.accessCounter,
|
|
75
|
+
accessCount: 1
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const wasNew = !this.cache.has(key);
|
|
79
|
+
this.cache.set(key, entry);
|
|
80
|
+
|
|
81
|
+
if (wasNew) {
|
|
82
|
+
this.stats.size++;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.updateMemoryUsage();
|
|
86
|
+
|
|
87
|
+
// Evict if necessary
|
|
88
|
+
await this.evictIfNeeded();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async delete(key: string | string[]): Promise<void> {
|
|
92
|
+
const keys = Array.isArray(key) ? key : [key];
|
|
93
|
+
let deletedCount = 0;
|
|
94
|
+
|
|
95
|
+
for (const k of keys) {
|
|
96
|
+
if (this.cache.delete(k)) {
|
|
97
|
+
deletedCount++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.stats.size -= deletedCount;
|
|
102
|
+
this.updateMemoryUsage();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async clear(): Promise<void> {
|
|
106
|
+
this.cache.clear();
|
|
107
|
+
this.stats.size = 0;
|
|
108
|
+
this.stats.memoryUsage = 0;
|
|
109
|
+
this.stats.hits = 0;
|
|
110
|
+
this.stats.misses = 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async getMany<T>(keys: string[]): Promise<(T | null)[]> {
|
|
114
|
+
const results: (T | null)[] = [];
|
|
115
|
+
|
|
116
|
+
for (const key of keys) {
|
|
117
|
+
const value = await this.get<T>(key);
|
|
118
|
+
results.push(value);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async setMany<T>(entries: Array<{key: string, value: T, ttl?: number}>): Promise<void> {
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
await this.set(entry.key, entry.value, entry.ttl);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async deleteMany(keys: string[]): Promise<void> {
|
|
131
|
+
return this.delete(keys);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async invalidatePattern(pattern: string): Promise<void> {
|
|
135
|
+
// Simple pattern matching - convert glob to regex
|
|
136
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'));
|
|
137
|
+
|
|
138
|
+
const keysToDelete: string[] = [];
|
|
139
|
+
for (const key of Array.from(this.cache.keys())) {
|
|
140
|
+
if (regex.test(key)) {
|
|
141
|
+
keysToDelete.push(key);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await this.delete(keysToDelete);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async ping(): Promise<boolean> {
|
|
149
|
+
return true; // Memory cache is always available
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async getStats(): Promise<CacheStats> {
|
|
153
|
+
const totalRequests = this.stats.hits + this.stats.misses;
|
|
154
|
+
const hitRate = totalRequests > 0 ? this.stats.hits / totalRequests : 0;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
hits: this.stats.hits,
|
|
158
|
+
misses: this.stats.misses,
|
|
159
|
+
hitRate,
|
|
160
|
+
size: this.stats.size,
|
|
161
|
+
memoryUsage: this.stats.memoryUsage
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private updateMemoryUsage(): void {
|
|
166
|
+
// Rough estimation of memory usage
|
|
167
|
+
// Each entry has overhead for the key, value, and metadata
|
|
168
|
+
let memoryUsage = 0;
|
|
169
|
+
for (const [key, entry] of Array.from(this.cache.entries())) {
|
|
170
|
+
memoryUsage += key.length * 2; // Rough string overhead
|
|
171
|
+
memoryUsage += this.estimateValueSize(entry.value);
|
|
172
|
+
memoryUsage += 100; // Overhead for entry metadata
|
|
173
|
+
}
|
|
174
|
+
this.stats.memoryUsage = memoryUsage;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private estimateValueSize(value: any): number {
|
|
178
|
+
if (value === null || value === undefined) return 8;
|
|
179
|
+
if (typeof value === 'string') return value.length * 2;
|
|
180
|
+
if (typeof value === 'number') return 8;
|
|
181
|
+
if (typeof value === 'boolean') return 1;
|
|
182
|
+
if (Array.isArray(value)) {
|
|
183
|
+
return value.reduce((size, item) => size + this.estimateValueSize(item), 16); // Array overhead
|
|
184
|
+
}
|
|
185
|
+
if (typeof value === 'object') {
|
|
186
|
+
let size = 16; // Object overhead
|
|
187
|
+
for (const key in value) {
|
|
188
|
+
size += key.length * 2 + this.estimateValueSize(value[key]);
|
|
189
|
+
}
|
|
190
|
+
return size;
|
|
191
|
+
}
|
|
192
|
+
return 16; // Default size for other types
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private async evictIfNeeded(): Promise<void> {
|
|
196
|
+
// Check size limit
|
|
197
|
+
if (this.stats.size > this.config.maxSize) {
|
|
198
|
+
await this.evictLRU(Math.ceil(this.config.maxSize * 0.1)); // Evict 10% of max size
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check memory limit
|
|
202
|
+
if (this.stats.memoryUsage > this.config.maxMemory) {
|
|
203
|
+
await this.evictLRU(Math.ceil(this.config.maxSize * 0.1)); // Evict 10% of max size
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private async evictLRU(count: number): Promise<void> {
|
|
208
|
+
// Sort entries by last accessed time (oldest first)
|
|
209
|
+
const entries = Array.from(this.cache.entries())
|
|
210
|
+
.map(([key, entry]) => ({ key, entry }))
|
|
211
|
+
.sort((a, b) => a.entry.lastAccessed - b.entry.lastAccessed);
|
|
212
|
+
|
|
213
|
+
const keysToDelete = entries.slice(0, count).map(item => item.key);
|
|
214
|
+
await this.delete(keysToDelete);
|
|
215
|
+
|
|
216
|
+
logger.debug(`Evicted ${keysToDelete.length} entries from cache due to LRU policy`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private startCleanupTimer(): void {
|
|
220
|
+
this.cleanupTimer = setInterval(() => {
|
|
221
|
+
this.cleanupExpired();
|
|
222
|
+
}, this.config.cleanupInterval);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private cleanupExpired(): void {
|
|
226
|
+
const now = Date.now();
|
|
227
|
+
const keysToDelete: string[] = [];
|
|
228
|
+
|
|
229
|
+
for (const [key, entry] of Array.from(this.cache.entries())) {
|
|
230
|
+
if (entry.expiresAt && now > entry.expiresAt) {
|
|
231
|
+
keysToDelete.push(key);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (keysToDelete.length > 0) {
|
|
236
|
+
this.delete(keysToDelete).catch(error => {
|
|
237
|
+
logger.error('Error during cache cleanup:', error);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Stop the cleanup timer (useful for testing or shutdown)
|
|
244
|
+
*/
|
|
245
|
+
stopCleanup(): void {
|
|
246
|
+
if (this.cleanupTimer) {
|
|
247
|
+
clearInterval(this.cleanupTimer);
|
|
248
|
+
this.cleanupTimer = undefined;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { CacheProvider, CacheStats } from './CacheProvider.js';
|
|
2
|
+
import type { CacheConfig } from '../../config/cache.config.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MultiLevelCache implements a two-tier caching strategy with L1 in-memory cache
|
|
6
|
+
* and L2 persistent cache (Redis). This provides optimal performance by serving
|
|
7
|
+
* frequently accessed data from memory while maintaining persistence across requests.
|
|
8
|
+
*
|
|
9
|
+
* Key Features:
|
|
10
|
+
* - L1 MemoryCache for fast access to hot data
|
|
11
|
+
* - L2 RedisCache for persistence and cross-instance sharing
|
|
12
|
+
* - Automatic L1 promotion on L2 cache hits
|
|
13
|
+
* - Configurable TTL strategies for each level
|
|
14
|
+
* - Write-through strategy for data consistency
|
|
15
|
+
*/
|
|
16
|
+
export class MultiLevelCache implements CacheProvider {
|
|
17
|
+
private l1Cache: CacheProvider;
|
|
18
|
+
private l2Cache: CacheProvider | null;
|
|
19
|
+
private config: CacheConfig;
|
|
20
|
+
|
|
21
|
+
constructor(l1Cache: CacheProvider, l2Cache: CacheProvider | null, config: CacheConfig) {
|
|
22
|
+
this.l1Cache = l1Cache;
|
|
23
|
+
this.l2Cache = l2Cache;
|
|
24
|
+
this.config = config;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getL1Cache(): CacheProvider {
|
|
28
|
+
return this.l1Cache;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getL2Cache(): CacheProvider | null {
|
|
32
|
+
return this.l2Cache;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async get(key: string): Promise<any | null> {
|
|
36
|
+
// Try L1 cache first
|
|
37
|
+
const l1Result = await this.l1Cache.get(key);
|
|
38
|
+
if (l1Result !== null) {
|
|
39
|
+
return l1Result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If L1 miss and L2 exists, try L2
|
|
43
|
+
if (this.l2Cache) {
|
|
44
|
+
const l2Result = await this.l2Cache.get(key);
|
|
45
|
+
if (l2Result !== null) {
|
|
46
|
+
// Promote to L1 cache for faster future access
|
|
47
|
+
await this.l1Cache.set(key, l2Result, this.config.defaultTTL);
|
|
48
|
+
return l2Result;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async set(key: string, value: any, ttl?: number): Promise<void> {
|
|
56
|
+
const effectiveTTL = ttl || this.config.defaultTTL;
|
|
57
|
+
|
|
58
|
+
// Set in L1 cache
|
|
59
|
+
await this.l1Cache.set(key, value, effectiveTTL);
|
|
60
|
+
|
|
61
|
+
// Set in L2 cache if available
|
|
62
|
+
if (this.l2Cache) {
|
|
63
|
+
await this.l2Cache.set(key, value, effectiveTTL);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async delete(key: string | string[]): Promise<void> {
|
|
68
|
+
// Delete from L1 cache
|
|
69
|
+
await this.l1Cache.delete(key);
|
|
70
|
+
|
|
71
|
+
// Delete from L2 cache if available
|
|
72
|
+
if (this.l2Cache) {
|
|
73
|
+
await this.l2Cache.delete(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async clear(): Promise<void> {
|
|
78
|
+
await this.l1Cache.clear();
|
|
79
|
+
if (this.l2Cache) {
|
|
80
|
+
await this.l2Cache.clear();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async getMany<T>(keys: string[]): Promise<(T | null)[]> {
|
|
85
|
+
const results: (T | null)[] = new Array(keys.length).fill(null);
|
|
86
|
+
const missingIndices: number[] = [];
|
|
87
|
+
const missingKeys: string[] = [];
|
|
88
|
+
|
|
89
|
+
// Try L1 cache first
|
|
90
|
+
const l1Results = await this.l1Cache.getMany<T>(keys);
|
|
91
|
+
for (let i = 0; i < keys.length; i++) {
|
|
92
|
+
const l1Value = l1Results[i];
|
|
93
|
+
const key = keys[i];
|
|
94
|
+
if (l1Value !== null && l1Value !== undefined) {
|
|
95
|
+
results[i] = l1Value;
|
|
96
|
+
} else if (key !== undefined) {
|
|
97
|
+
missingIndices.push(i);
|
|
98
|
+
missingKeys.push(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If L2 exists and we have missing keys, try L2
|
|
103
|
+
if (this.l2Cache && missingKeys.length > 0) {
|
|
104
|
+
const l2Results = await this.l2Cache.getMany<T>(missingKeys);
|
|
105
|
+
for (let i = 0; i < missingKeys.length; i++) {
|
|
106
|
+
const value = l2Results[i];
|
|
107
|
+
const originalIndex = missingIndices[i];
|
|
108
|
+
const missingKey = missingKeys[i];
|
|
109
|
+
if (value !== null && value !== undefined && originalIndex !== undefined && missingKey !== undefined) {
|
|
110
|
+
results[originalIndex] = value;
|
|
111
|
+
// Promote to L1 cache
|
|
112
|
+
await this.l1Cache.set(missingKey, value, this.config.defaultTTL);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return results;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async setMany<T>(entries: Array<{key: string, value: T, ttl?: number}>): Promise<void> {
|
|
121
|
+
// Apply default TTL to entries without one
|
|
122
|
+
const entriesWithTTL = entries.map(e => ({
|
|
123
|
+
...e,
|
|
124
|
+
ttl: e.ttl || this.config.defaultTTL
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
// Set in L1 cache
|
|
128
|
+
await this.l1Cache.setMany(entriesWithTTL);
|
|
129
|
+
|
|
130
|
+
// Set in L2 cache if available
|
|
131
|
+
if (this.l2Cache) {
|
|
132
|
+
await this.l2Cache.setMany(entriesWithTTL);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async deleteMany(keys: string[]): Promise<void> {
|
|
137
|
+
await this.l1Cache.deleteMany(keys);
|
|
138
|
+
if (this.l2Cache) {
|
|
139
|
+
await this.l2Cache.deleteMany(keys);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async invalidatePattern(pattern: string): Promise<void> {
|
|
144
|
+
await this.l1Cache.invalidatePattern(pattern);
|
|
145
|
+
if (this.l2Cache) {
|
|
146
|
+
await this.l2Cache.invalidatePattern(pattern);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async ping(): Promise<boolean> {
|
|
151
|
+
const l1Ping = await this.l1Cache.ping();
|
|
152
|
+
|
|
153
|
+
if (!this.l2Cache) {
|
|
154
|
+
return l1Ping;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const l2Ping = await this.l2Cache.ping();
|
|
158
|
+
|
|
159
|
+
// Multi-level cache is healthy if both levels are healthy
|
|
160
|
+
return l1Ping && l2Ping;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async getStats(): Promise<CacheStats> {
|
|
164
|
+
const l1Stats = await this.l1Cache.getStats();
|
|
165
|
+
const l2Stats = this.l2Cache ? await this.l2Cache.getStats() : null;
|
|
166
|
+
|
|
167
|
+
const totalHits = l1Stats.hits + (l2Stats?.hits || 0);
|
|
168
|
+
const totalMisses = l1Stats.misses + (l2Stats?.misses || 0);
|
|
169
|
+
const totalRequests = totalHits + totalMisses;
|
|
170
|
+
const hitRate = totalRequests > 0 ? totalHits / totalRequests : 0;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
hits: totalHits,
|
|
174
|
+
misses: totalMisses,
|
|
175
|
+
hitRate,
|
|
176
|
+
size: l1Stats.size + (l2Stats?.size || 0),
|
|
177
|
+
memoryUsage: (l1Stats.memoryUsage || 0) + (l2Stats?.memoryUsage || 0)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|