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,254 @@
|
|
|
1
|
+
import type { CacheProvider, CacheStats } from './CacheProvider.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TTLStrategy implements dynamic TTL management based on data access patterns.
|
|
5
|
+
* Tracks access frequency and adjusts TTL accordingly to optimize cache efficiency.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Hot data detection: doubles TTL for frequently accessed data
|
|
9
|
+
* - Cold data detection: halves TTL for rarely accessed data
|
|
10
|
+
* - Access pattern tracking with sliding window
|
|
11
|
+
* - Configurable thresholds for hot/cold classification
|
|
12
|
+
*/
|
|
13
|
+
export class TTLStrategy {
|
|
14
|
+
private accessCounts: Map<string, number> = new Map();
|
|
15
|
+
private lastAccessTime: Map<string, number> = new Map();
|
|
16
|
+
private baseTTL: number;
|
|
17
|
+
private hotThreshold: number;
|
|
18
|
+
private coldThreshold: number;
|
|
19
|
+
private windowSize: number;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
baseTTL: number = 3600000, // 1 hour default
|
|
23
|
+
hotThreshold: number = 10, // 10 accesses = hot
|
|
24
|
+
coldThreshold: number = 1, // 1 access = cold
|
|
25
|
+
windowSize: number = 300000 // 5 minutes window
|
|
26
|
+
) {
|
|
27
|
+
this.baseTTL = baseTTL;
|
|
28
|
+
this.hotThreshold = hotThreshold;
|
|
29
|
+
this.coldThreshold = coldThreshold;
|
|
30
|
+
this.windowSize = windowSize;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Records an access to a cache key and returns the appropriate TTL
|
|
35
|
+
*/
|
|
36
|
+
recordAccess(key: string): number {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
const lastAccess = this.lastAccessTime.get(key) || 0;
|
|
39
|
+
const timeDiff = now - lastAccess;
|
|
40
|
+
|
|
41
|
+
// Reset count if outside window
|
|
42
|
+
if (timeDiff > this.windowSize) {
|
|
43
|
+
this.accessCounts.set(key, 1);
|
|
44
|
+
} else {
|
|
45
|
+
const currentCount = this.accessCounts.get(key) || 0;
|
|
46
|
+
this.accessCounts.set(key, currentCount + 1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.lastAccessTime.set(key, now);
|
|
50
|
+
|
|
51
|
+
const accessCount = this.accessCounts.get(key) || 0;
|
|
52
|
+
return this.calculateTTL(accessCount);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Calculates TTL based on access frequency
|
|
57
|
+
*/
|
|
58
|
+
private calculateTTL(accessCount: number): number {
|
|
59
|
+
if (accessCount >= this.hotThreshold) {
|
|
60
|
+
// Hot data: double TTL
|
|
61
|
+
return this.baseTTL * 2;
|
|
62
|
+
} else if (accessCount <= this.coldThreshold) {
|
|
63
|
+
// Cold data: halve TTL
|
|
64
|
+
return Math.max(this.baseTTL / 2, 60000); // Minimum 1 minute
|
|
65
|
+
} else {
|
|
66
|
+
// Normal data: base TTL
|
|
67
|
+
return this.baseTTL;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Gets current access statistics for a key
|
|
73
|
+
*/
|
|
74
|
+
getAccessStats(key: string): { count: number; lastAccess: number; ttl: number; category: 'hot' | 'cold' | 'normal' } {
|
|
75
|
+
const count = this.accessCounts.get(key) || 0;
|
|
76
|
+
const lastAccess = this.lastAccessTime.get(key) || 0;
|
|
77
|
+
const ttl = this.calculateTTL(count);
|
|
78
|
+
|
|
79
|
+
let category: 'hot' | 'cold' | 'normal';
|
|
80
|
+
if (count >= this.hotThreshold) {
|
|
81
|
+
category = 'hot';
|
|
82
|
+
} else if (count <= this.coldThreshold) {
|
|
83
|
+
category = 'cold';
|
|
84
|
+
} else {
|
|
85
|
+
category = 'normal';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { count, lastAccess, ttl, category };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Cleans up old access records to prevent memory leaks
|
|
93
|
+
*/
|
|
94
|
+
cleanup(maxAge: number = 3600000): void { // 1 hour default
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
const keysToDelete: string[] = [];
|
|
97
|
+
|
|
98
|
+
for (const [key, lastAccess] of this.lastAccessTime) {
|
|
99
|
+
if (now - lastAccess > maxAge) {
|
|
100
|
+
keysToDelete.push(key);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const key of keysToDelete) {
|
|
105
|
+
this.accessCounts.delete(key);
|
|
106
|
+
this.lastAccessTime.delete(key);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Gets overall statistics
|
|
112
|
+
*/
|
|
113
|
+
getStats(): {
|
|
114
|
+
totalKeys: number;
|
|
115
|
+
hotKeys: number;
|
|
116
|
+
coldKeys: number;
|
|
117
|
+
normalKeys: number;
|
|
118
|
+
averageAccessCount: number;
|
|
119
|
+
} {
|
|
120
|
+
let hotKeys = 0;
|
|
121
|
+
let coldKeys = 0;
|
|
122
|
+
let normalKeys = 0;
|
|
123
|
+
let totalAccessCount = 0;
|
|
124
|
+
|
|
125
|
+
for (const [key, count] of this.accessCounts) {
|
|
126
|
+
totalAccessCount += count;
|
|
127
|
+
|
|
128
|
+
if (count >= this.hotThreshold) {
|
|
129
|
+
hotKeys++;
|
|
130
|
+
} else if (count <= this.coldThreshold) {
|
|
131
|
+
coldKeys++;
|
|
132
|
+
} else {
|
|
133
|
+
normalKeys++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const totalKeys = this.accessCounts.size;
|
|
138
|
+
const averageAccessCount = totalKeys > 0 ? totalAccessCount / totalKeys : 0;
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
totalKeys,
|
|
142
|
+
hotKeys,
|
|
143
|
+
coldKeys,
|
|
144
|
+
normalKeys,
|
|
145
|
+
averageAccessCount
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resets all tracking data
|
|
151
|
+
*/
|
|
152
|
+
reset(): void {
|
|
153
|
+
this.accessCounts.clear();
|
|
154
|
+
this.lastAccessTime.clear();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Enhanced cache provider that integrates TTL strategy
|
|
160
|
+
*/
|
|
161
|
+
export class AdaptiveTTLProvider implements CacheProvider {
|
|
162
|
+
private cache: CacheProvider;
|
|
163
|
+
private ttlStrategy: TTLStrategy;
|
|
164
|
+
|
|
165
|
+
constructor(cache: CacheProvider, ttlStrategy: TTLStrategy) {
|
|
166
|
+
this.cache = cache;
|
|
167
|
+
this.ttlStrategy = ttlStrategy;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async get(key: string): Promise<any | null> {
|
|
171
|
+
const result = await this.cache.get(key);
|
|
172
|
+
if (result !== null) {
|
|
173
|
+
// Record access for TTL adjustment
|
|
174
|
+
this.ttlStrategy.recordAccess(key);
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async set(key: string, value: any, ttl?: number): Promise<void> {
|
|
180
|
+
// Use TTL strategy if no explicit TTL provided
|
|
181
|
+
const effectiveTTL = ttl || this.ttlStrategy.recordAccess(key);
|
|
182
|
+
await this.cache.set(key, value, effectiveTTL);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async delete(key: string | string[]): Promise<void> {
|
|
186
|
+
await this.cache.delete(key);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async clear(): Promise<void> {
|
|
190
|
+
await this.cache.clear();
|
|
191
|
+
this.ttlStrategy.reset();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async getMany<T>(keys: string[]): Promise<(T | null)[]> {
|
|
195
|
+
const results = await this.cache.getMany<T>(keys);
|
|
196
|
+
|
|
197
|
+
// Record access for found keys
|
|
198
|
+
for (let i = 0; i < keys.length; i++) {
|
|
199
|
+
const key = keys[i];
|
|
200
|
+
if (results[i] !== null && key !== undefined) {
|
|
201
|
+
this.ttlStrategy.recordAccess(key);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return results;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async setMany<T>(entries: Array<{key: string, value: T, ttl?: number}>): Promise<void> {
|
|
209
|
+
// Apply adaptive TTL to entries without explicit TTL
|
|
210
|
+
const entriesWithTTL = entries.map(entry => ({
|
|
211
|
+
...entry,
|
|
212
|
+
ttl: entry.ttl || this.ttlStrategy.recordAccess(entry.key)
|
|
213
|
+
}));
|
|
214
|
+
|
|
215
|
+
await this.cache.setMany(entriesWithTTL);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async deleteMany(keys: string[]): Promise<void> {
|
|
219
|
+
await this.cache.deleteMany(keys);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async invalidatePattern(pattern: string): Promise<void> {
|
|
223
|
+
await this.cache.invalidatePattern(pattern);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async ping(): Promise<boolean> {
|
|
227
|
+
return await this.cache.ping();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async getStats(): Promise<CacheStats> {
|
|
231
|
+
return await this.cache.getStats();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Gets TTL strategy statistics
|
|
236
|
+
*/
|
|
237
|
+
getTTLStats() {
|
|
238
|
+
return this.ttlStrategy.getStats();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Gets access stats for a specific key
|
|
243
|
+
*/
|
|
244
|
+
getKeyAccessStats(key: string) {
|
|
245
|
+
return this.ttlStrategy.getAccessStats(key);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Cleans up old access records
|
|
250
|
+
*/
|
|
251
|
+
cleanupAccessRecords(maxAge?: number) {
|
|
252
|
+
this.ttlStrategy.cleanup(maxAge);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { CacheProvider, CacheStats } from './CacheProvider';
|
|
2
|
+
export { MemoryCache } from './MemoryCache';
|
|
3
|
+
export type { MemoryCacheConfig } from './MemoryCache';
|
|
4
|
+
export { NoOpCache } from './NoOpCache';
|
|
5
|
+
export { CacheManager } from './CacheManager';
|
|
6
|
+
export { CacheFactory } from './CacheFactory';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import "reflect-metadata";
|
|
3
|
+
import { logger as MainLogger } from "../Logger";
|
|
4
|
+
import ComponentRegistry from "./ComponentRegistry";
|
|
5
|
+
import { type ComponentDataType } from './Interfaces';
|
|
6
|
+
import { uuidv7 } from '../../utils/uuid';
|
|
7
|
+
import { getMetadataStorage } from '../metadata';
|
|
8
|
+
const logger = MainLogger.child({ scope: "Components" });
|
|
9
|
+
|
|
10
|
+
export class BaseComponent {
|
|
11
|
+
public id: string = "";
|
|
12
|
+
protected _comp_name: string = "";
|
|
13
|
+
protected _typeId: string = "";
|
|
14
|
+
protected _persisted: boolean = false;
|
|
15
|
+
protected _dirty: boolean = false;
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
this._comp_name = this.constructor.name;
|
|
19
|
+
const storage = getMetadataStorage();
|
|
20
|
+
this._typeId = storage.getComponentId(this._comp_name);
|
|
21
|
+
this._dirty = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getTypeID(): string {
|
|
25
|
+
return this._typeId;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
properties(): string[] {
|
|
29
|
+
const storage = getMetadataStorage();
|
|
30
|
+
const props = storage.componentProperties.get(this._typeId);
|
|
31
|
+
if(!props) return [];
|
|
32
|
+
return props.map(p => p.propertyKey);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get data for this component
|
|
37
|
+
* @returns Object containing only properties marked with @CompData decorator
|
|
38
|
+
*/
|
|
39
|
+
data<T extends this>(): ComponentDataType<T> {
|
|
40
|
+
const data: Record<string, any> = {};
|
|
41
|
+
this.properties().forEach((prop: string) => {
|
|
42
|
+
data[prop] = (this as any)[prop];
|
|
43
|
+
});
|
|
44
|
+
return data as ComponentDataType<T>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get serializable data for database storage
|
|
49
|
+
* @returns Object with Dates serialized to ISO strings
|
|
50
|
+
*/
|
|
51
|
+
serializableData(): Record<string, any> {
|
|
52
|
+
const data: Record<string, any> = {};
|
|
53
|
+
const storage = getMetadataStorage();
|
|
54
|
+
const props = storage.componentProperties.get(this._typeId);
|
|
55
|
+
this.properties().forEach((prop: string) => {
|
|
56
|
+
let value = (this as any)[prop];
|
|
57
|
+
const propMeta = props?.find(p => p.propertyKey === prop);
|
|
58
|
+
if (propMeta?.propertyType === Date && value instanceof Date) {
|
|
59
|
+
value = value.toISOString();
|
|
60
|
+
}
|
|
61
|
+
data[prop] = value;
|
|
62
|
+
});
|
|
63
|
+
return data;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async save(trx: Bun.SQL, entity_id: string) {
|
|
67
|
+
logger.trace(`Saving component ${this._comp_name} for entity ${entity_id}`);
|
|
68
|
+
// Only check readiness if component is not yet registered
|
|
69
|
+
// This optimization avoids 40,000+ unnecessary async calls for bulk operations
|
|
70
|
+
if(!ComponentRegistry.isComponentReady(this._comp_name)) {
|
|
71
|
+
logger.trace(`Checking is Component can be saved (is registered)`);
|
|
72
|
+
await ComponentRegistry.getReadyPromise(this._comp_name);
|
|
73
|
+
logger.trace(`Component Registered`);
|
|
74
|
+
}
|
|
75
|
+
if(this._persisted) {
|
|
76
|
+
await this.update(trx);
|
|
77
|
+
} else {
|
|
78
|
+
await this.insert(trx, entity_id);
|
|
79
|
+
this._persisted = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async insert(trx: Bun.SQL, entity_id: string) {
|
|
84
|
+
if(this.id === "") {
|
|
85
|
+
this.id = uuidv7();
|
|
86
|
+
}
|
|
87
|
+
// Validate entity_id to prevent PostgreSQL UUID parsing errors
|
|
88
|
+
if (!entity_id || entity_id.trim() === '') {
|
|
89
|
+
throw new Error(`Cannot insert component ${this._comp_name}: entity_id is empty or invalid`);
|
|
90
|
+
}
|
|
91
|
+
await trx`INSERT INTO components
|
|
92
|
+
(id, entity_id, name, type_id, data)
|
|
93
|
+
VALUES (${this.id}, ${entity_id}, ${this._comp_name}, ${this._typeId}, ${this.serializableData()})`
|
|
94
|
+
await trx`INSERT INTO entity_components (entity_id, type_id, component_id) VALUES (${entity_id}, ${this._typeId}, ${this.id}) ON CONFLICT DO NOTHING`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async update(trx: Bun.SQL) {
|
|
98
|
+
if(this.id === "") {
|
|
99
|
+
throw new Error("Component must have an ID to be updated");
|
|
100
|
+
}
|
|
101
|
+
await trx`UPDATE components SET data = ${this.serializableData()} WHERE id = ${this.id}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public setPersisted(persisted: boolean) {
|
|
105
|
+
this._persisted = persisted;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public setDirty(dirty: boolean) {
|
|
109
|
+
this._dirty = dirty;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
indexedProperties(): string[] {
|
|
113
|
+
const storage = getMetadataStorage();
|
|
114
|
+
const props = storage.componentProperties.get(this._typeId);
|
|
115
|
+
if(!props) return [];
|
|
116
|
+
return props.filter(p => p.indexed).map(p => p.propertyKey);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default BaseComponent;
|