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,520 @@
|
|
|
1
|
+
import { type CacheProvider } from './CacheProvider';
|
|
2
|
+
import { type CacheConfig, defaultCacheConfig } from '../../config/cache.config';
|
|
3
|
+
import { CacheFactory } from './CacheFactory';
|
|
4
|
+
import { MultiLevelCache } from './MultiLevelCache';
|
|
5
|
+
import { RedisCache } from './RedisCache';
|
|
6
|
+
import { logger } from '../Logger';
|
|
7
|
+
import type { Entity } from '../Entity';
|
|
8
|
+
import type { BaseComponent } from '../components';
|
|
9
|
+
import type { ComponentData } from '../RequestLoaders';
|
|
10
|
+
|
|
11
|
+
interface InvalidationMessage {
|
|
12
|
+
instanceId: string;
|
|
13
|
+
type: 'key' | 'pattern';
|
|
14
|
+
keys?: string[];
|
|
15
|
+
pattern?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* High-level cache operations manager
|
|
20
|
+
* Singleton that provides entity and component caching methods
|
|
21
|
+
* Note: Query-level caching has been removed in favor of component-level caching only
|
|
22
|
+
*/
|
|
23
|
+
export class CacheManager {
|
|
24
|
+
private static instance: CacheManager;
|
|
25
|
+
private provider: CacheProvider;
|
|
26
|
+
private config: CacheConfig;
|
|
27
|
+
private instanceId = crypto.randomUUID();
|
|
28
|
+
private pubSubEnabled = false;
|
|
29
|
+
private static readonly INVALIDATION_CHANNEL = 'bunsane:cache:invalidate';
|
|
30
|
+
|
|
31
|
+
private constructor() {
|
|
32
|
+
this.config = defaultCacheConfig;
|
|
33
|
+
this.provider = CacheFactory.create(this.config);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public static getInstance(): CacheManager {
|
|
37
|
+
if (!CacheManager.instance) {
|
|
38
|
+
CacheManager.instance = new CacheManager();
|
|
39
|
+
}
|
|
40
|
+
return CacheManager.instance;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Initialize or reinitialize the cache manager with new config
|
|
45
|
+
*/
|
|
46
|
+
public async initialize(config: Partial<CacheConfig>): Promise<void> {
|
|
47
|
+
// Shutdown old provider before replacing
|
|
48
|
+
await this.shutdownProvider();
|
|
49
|
+
this.pubSubEnabled = false;
|
|
50
|
+
|
|
51
|
+
this.config = { ...defaultCacheConfig, ...config };
|
|
52
|
+
this.provider = CacheFactory.create(this.config);
|
|
53
|
+
|
|
54
|
+
await this.setupPubSub();
|
|
55
|
+
|
|
56
|
+
logger.info({ scope: 'cache', component: 'CacheManager', msg: 'CacheManager initialized', provider: this.config.provider, enabled: this.config.enabled });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the current cache configuration
|
|
61
|
+
*/
|
|
62
|
+
public getConfig(): CacheConfig {
|
|
63
|
+
return { ...this.config };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the current cache provider
|
|
68
|
+
*/
|
|
69
|
+
public getProvider(): CacheProvider {
|
|
70
|
+
return this.provider;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Entity caching methods
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get an entity existence check from cache
|
|
77
|
+
* Returns entity ID if exists, null if not found
|
|
78
|
+
*/
|
|
79
|
+
public async getEntity(id: string): Promise<string | null> {
|
|
80
|
+
if (!this.config.enabled || !this.config.entity?.enabled) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const key = `entity:${id}`;
|
|
86
|
+
const result = await this.provider.get<string>(key);
|
|
87
|
+
return result || null;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error getting entity from cache', error });
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Set an entity existence in cache with write-through strategy
|
|
96
|
+
* Only caches entity ID for existence tracking, not full entity data
|
|
97
|
+
*/
|
|
98
|
+
public async setEntityWriteThrough(entity: Entity, ttl?: number): Promise<void> {
|
|
99
|
+
if (!this.config.enabled || !this.config.entity?.enabled) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const key = `entity:${entity.id}`;
|
|
105
|
+
const effectiveTTL = ttl ?? this.config.entity.ttl;
|
|
106
|
+
// Only cache entity ID for existence check
|
|
107
|
+
await this.provider.set(key, entity.id, effectiveTTL);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error setting entity in cache', error });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Invalidate an entity from cache
|
|
115
|
+
*/
|
|
116
|
+
public async invalidateEntity(id: string): Promise<void> {
|
|
117
|
+
if (!this.config.enabled || !this.config.entity?.enabled) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const key = `entity:${id}`;
|
|
123
|
+
await this.provider.delete(key);
|
|
124
|
+
await this.publishInvalidation('key', [key]);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error invalidating entity from cache', error });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get multiple entity existence checks from cache (for DataLoader integration)
|
|
132
|
+
* Returns entity IDs if they exist, null if not found
|
|
133
|
+
*/
|
|
134
|
+
public async getEntities(ids: string[]): Promise<(string | null)[]> {
|
|
135
|
+
if (!this.config.enabled || !this.config.entity?.enabled) {
|
|
136
|
+
return ids.map(() => null);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const cacheKeys = ids.map(id => `entity:${id}`);
|
|
141
|
+
const results = await this.provider.getMany<string>(cacheKeys);
|
|
142
|
+
return results;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error getting entities from cache', error });
|
|
145
|
+
return ids.map(() => null);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Set multiple entity existences in cache with write-through strategy (for DataLoader integration)
|
|
151
|
+
* Only caches entity IDs for existence tracking, not full entity data
|
|
152
|
+
*/
|
|
153
|
+
public async setEntitiesWriteThrough(entities: Entity[], ttl?: number): Promise<void> {
|
|
154
|
+
if (!this.config.enabled || !this.config.entity?.enabled) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const effectiveTTL = ttl ?? this.config.entity?.ttl;
|
|
160
|
+
const entries = entities.map(entity => ({
|
|
161
|
+
key: `entity:${entity.id}`,
|
|
162
|
+
// Only cache entity ID for existence check
|
|
163
|
+
value: entity.id,
|
|
164
|
+
ttl: effectiveTTL
|
|
165
|
+
}));
|
|
166
|
+
await this.provider.setMany(entries);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error setting entities in cache', error });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Component caching methods
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get components for an entity from cache
|
|
176
|
+
*/
|
|
177
|
+
public async getComponentsByEntity(entityId: string, componentType?: string): Promise<BaseComponent[] | null> {
|
|
178
|
+
if (!this.config.enabled || !this.config.component?.enabled) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const key = componentType
|
|
184
|
+
? `component:${entityId}:${componentType}`
|
|
185
|
+
: `components:${entityId}`;
|
|
186
|
+
return await this.provider.get<BaseComponent[]>(key);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error getting components from cache', error });
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Set components for an entity in cache with write-through strategy.
|
|
195
|
+
* Converts BaseComponent instances to ComponentData format for cache compatibility with DataLoader.
|
|
196
|
+
*/
|
|
197
|
+
public async setComponentWriteThrough(entityId: string, components: BaseComponent[], componentType?: string, ttl?: number): Promise<void> {
|
|
198
|
+
if (!this.config.enabled || !this.config.component?.enabled) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const effectiveTTL = ttl ?? this.config.component.ttl;
|
|
204
|
+
|
|
205
|
+
// Convert BaseComponent to ComponentData format for cache compatibility with DataLoader
|
|
206
|
+
for (const component of components) {
|
|
207
|
+
const typeId = componentType || component.getTypeID();
|
|
208
|
+
const key = `component:${entityId}:${typeId}`;
|
|
209
|
+
|
|
210
|
+
// Create ComponentData structure matching what DataLoader expects
|
|
211
|
+
const componentData: ComponentData = {
|
|
212
|
+
id: component.id,
|
|
213
|
+
entityId: entityId,
|
|
214
|
+
typeId: typeId,
|
|
215
|
+
data: component.data(),
|
|
216
|
+
createdAt: new Date(), // Component doesn't track this, use current time
|
|
217
|
+
updatedAt: new Date(),
|
|
218
|
+
deletedAt: null
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
await this.provider.set(key, componentData, effectiveTTL);
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error setting components in cache', error });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Invalidate a specific component for an entity from cache
|
|
230
|
+
* More granular than invalidateComponents which can invalidate all components
|
|
231
|
+
*/
|
|
232
|
+
public async invalidateComponent(entityId: string, typeId: string): Promise<void> {
|
|
233
|
+
if (!this.config.enabled || !this.config.component?.enabled) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
logger.trace({
|
|
239
|
+
msg: 'Invalidating component from cache',
|
|
240
|
+
entityId,
|
|
241
|
+
typeId
|
|
242
|
+
})
|
|
243
|
+
const key = `component:${entityId}:${typeId}`;
|
|
244
|
+
await this.provider.delete(key);
|
|
245
|
+
await this.publishInvalidation('key', [key]);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error invalidating component from cache', error });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Invalidate multiple specific components from cache
|
|
253
|
+
* Useful for bulk invalidation operations
|
|
254
|
+
*/
|
|
255
|
+
public async invalidateComponents(components: Array<{ entityId: string; typeId: string }>): Promise<void> {
|
|
256
|
+
if (!this.config.enabled || !this.config.component?.enabled) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const keys = components.map(comp => `component:${comp.entityId}:${comp.typeId}`);
|
|
262
|
+
await this.provider.deleteMany(keys);
|
|
263
|
+
await this.publishInvalidation('key', keys);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error invalidating components from cache', error });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Invalidate all components for a specific entity from cache
|
|
271
|
+
* Uses pattern matching to efficiently clear all component caches for an entity
|
|
272
|
+
*/
|
|
273
|
+
public async invalidateAllEntityComponents(entityId: string): Promise<void> {
|
|
274
|
+
if (!this.config.enabled || !this.config.component?.enabled) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const pattern = `component:${entityId}:*`;
|
|
280
|
+
await this.provider.invalidatePattern(pattern);
|
|
281
|
+
await this.publishInvalidation('pattern', undefined, pattern);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error invalidating all entity components from cache', error });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get components by entity and type from cache (for DataLoader integration)
|
|
289
|
+
*/
|
|
290
|
+
public async getComponents(keys: Array<{ entityId: string; typeId: string }>): Promise<(ComponentData | null)[]> {
|
|
291
|
+
if (!this.config.enabled || !this.config.component?.enabled) {
|
|
292
|
+
return keys.map(() => null);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const cacheKeys = keys.map(k => `component:${k.entityId}:${k.typeId}`);
|
|
297
|
+
const results = await this.provider.getMany<ComponentData>(cacheKeys);
|
|
298
|
+
return results;
|
|
299
|
+
} catch (error) {
|
|
300
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error getting components from cache', error });
|
|
301
|
+
return keys.map(() => null);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Set components in cache with write-through strategy (for DataLoader integration)
|
|
307
|
+
*/
|
|
308
|
+
public async setComponentsWriteThrough(components: ComponentData[], ttl?: number): Promise<void> {
|
|
309
|
+
if (!this.config.enabled || !this.config.component?.enabled) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const effectiveTTL = ttl ?? this.config.component?.ttl;
|
|
315
|
+
const entries = components.map(comp => ({
|
|
316
|
+
key: `component:${comp.entityId}:${comp.typeId}`,
|
|
317
|
+
value: comp,
|
|
318
|
+
ttl: effectiveTTL
|
|
319
|
+
}));
|
|
320
|
+
await this.provider.setMany(entries);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error setting components in cache', error });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Generic cache methods
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Generic get method
|
|
330
|
+
*/
|
|
331
|
+
public async get<T>(key: string): Promise<T | null> {
|
|
332
|
+
if (!this.config.enabled) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
return await this.provider.get<T>(key);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error getting from cache', error });
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Generic set method
|
|
346
|
+
*/
|
|
347
|
+
public async set<T>(key: string, value: T, ttl?: number): Promise<void> {
|
|
348
|
+
if (!this.config.enabled) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const effectiveTTL = ttl ?? this.config.defaultTTL;
|
|
354
|
+
await this.provider.set(key, value, effectiveTTL);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error setting in cache', error });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Generic delete method
|
|
362
|
+
*/
|
|
363
|
+
public async delete(key: string | string[]): Promise<void> {
|
|
364
|
+
if (!this.config.enabled) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
await this.provider.delete(key);
|
|
370
|
+
const keys = Array.isArray(key) ? key : [key];
|
|
371
|
+
await this.publishInvalidation('key', keys);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error deleting from cache', error });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Clear all cache
|
|
379
|
+
*/
|
|
380
|
+
public async clear(): Promise<void> {
|
|
381
|
+
if (!this.config.enabled) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
await this.provider.clear();
|
|
387
|
+
await this.publishInvalidation('pattern', undefined, '*');
|
|
388
|
+
} catch (error) {
|
|
389
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error clearing cache', error });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Get cache statistics
|
|
395
|
+
*/
|
|
396
|
+
public async getStats() {
|
|
397
|
+
try {
|
|
398
|
+
return await this.provider.getStats();
|
|
399
|
+
} catch (error) {
|
|
400
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error getting cache stats', error });
|
|
401
|
+
return {
|
|
402
|
+
hits: 0,
|
|
403
|
+
misses: 0,
|
|
404
|
+
hitRate: 0,
|
|
405
|
+
size: 0,
|
|
406
|
+
memoryUsage: 0
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Health check for cache
|
|
413
|
+
*/
|
|
414
|
+
public async ping(): Promise<boolean> {
|
|
415
|
+
try {
|
|
416
|
+
return await this.provider.ping();
|
|
417
|
+
} catch (error) {
|
|
418
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Cache ping failed', error });
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// --- Cross-instance pub/sub ---
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Setup pub/sub for cross-instance cache invalidation.
|
|
427
|
+
* Only activates when using MultiLevel provider with a Redis L2.
|
|
428
|
+
*/
|
|
429
|
+
private async setupPubSub(): Promise<void> {
|
|
430
|
+
if (!(this.provider instanceof MultiLevelCache)) return;
|
|
431
|
+
|
|
432
|
+
const l2 = this.provider.getL2Cache();
|
|
433
|
+
if (!(l2 instanceof RedisCache)) return;
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
await l2.subscribeInvalidation(
|
|
437
|
+
CacheManager.INVALIDATION_CHANNEL,
|
|
438
|
+
(_channel, message) => this.handleRemoteInvalidation(message)
|
|
439
|
+
);
|
|
440
|
+
this.pubSubEnabled = true;
|
|
441
|
+
logger.info({ scope: 'cache', component: 'CacheManager', msg: 'Cross-instance cache invalidation enabled', instanceId: this.instanceId });
|
|
442
|
+
} catch (error) {
|
|
443
|
+
logger.warn({ scope: 'cache', component: 'CacheManager', msg: 'Failed to setup pub/sub', error });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Handle an invalidation message from another instance.
|
|
449
|
+
* Ignores messages from self. Invalidates L1 only (L2 is shared Redis).
|
|
450
|
+
*/
|
|
451
|
+
private async handleRemoteInvalidation(raw: string): Promise<void> {
|
|
452
|
+
try {
|
|
453
|
+
const msg: InvalidationMessage = JSON.parse(raw);
|
|
454
|
+
|
|
455
|
+
// Ignore our own messages
|
|
456
|
+
if (msg.instanceId === this.instanceId) return;
|
|
457
|
+
|
|
458
|
+
if (!(this.provider instanceof MultiLevelCache)) return;
|
|
459
|
+
const l1 = this.provider.getL1Cache();
|
|
460
|
+
|
|
461
|
+
if (msg.type === 'key' && msg.keys) {
|
|
462
|
+
await l1.deleteMany(msg.keys);
|
|
463
|
+
} else if (msg.type === 'pattern' && msg.pattern) {
|
|
464
|
+
await l1.invalidatePattern(msg.pattern);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
logger.debug({ scope: 'cache', component: 'CacheManager', msg: 'Applied remote invalidation', from: msg.instanceId, type: msg.type });
|
|
468
|
+
} catch (error) {
|
|
469
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error handling remote invalidation', error });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Publish an invalidation event to other instances via Redis pub/sub.
|
|
475
|
+
*/
|
|
476
|
+
private async publishInvalidation(type: 'key' | 'pattern', keys?: string[], pattern?: string): Promise<void> {
|
|
477
|
+
if (!this.pubSubEnabled) return;
|
|
478
|
+
if (!(this.provider instanceof MultiLevelCache)) return;
|
|
479
|
+
|
|
480
|
+
const l2 = this.provider.getL2Cache();
|
|
481
|
+
if (!(l2 instanceof RedisCache)) return;
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
const msg: InvalidationMessage = { instanceId: this.instanceId, type, keys, pattern };
|
|
485
|
+
await l2.publishInvalidation(CacheManager.INVALIDATION_CHANNEL, JSON.stringify(msg));
|
|
486
|
+
} catch (error) {
|
|
487
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error publishing invalidation', error });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Shutdown the current provider (disconnect Redis, stop Memory cleanup timer)
|
|
493
|
+
*/
|
|
494
|
+
private async shutdownProvider(): Promise<void> {
|
|
495
|
+
try {
|
|
496
|
+
const provider = this.provider as any;
|
|
497
|
+
// RedisCache has disconnect()
|
|
498
|
+
if (typeof provider.disconnect === 'function') {
|
|
499
|
+
await provider.disconnect();
|
|
500
|
+
}
|
|
501
|
+
// MemoryCache has stopCleanup()
|
|
502
|
+
if (typeof provider.stopCleanup === 'function') {
|
|
503
|
+
provider.stopCleanup();
|
|
504
|
+
}
|
|
505
|
+
} catch (error) {
|
|
506
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error shutting down provider', error });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Shutdown the cache manager
|
|
512
|
+
*/
|
|
513
|
+
public async shutdown(): Promise<void> {
|
|
514
|
+
try {
|
|
515
|
+
await this.shutdownProvider();
|
|
516
|
+
} catch (error) {
|
|
517
|
+
logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error shutting down cache', error });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Provider Interface for BunSane Framework
|
|
3
|
+
* Defines the contract for all cache implementations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface CacheStats {
|
|
7
|
+
hits: number;
|
|
8
|
+
misses: number;
|
|
9
|
+
hitRate: number;
|
|
10
|
+
size: number;
|
|
11
|
+
memoryUsage?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CacheProvider {
|
|
15
|
+
// Basic operations
|
|
16
|
+
get<T>(key: string): Promise<T | null>;
|
|
17
|
+
set<T>(key: string, value: T, ttl?: number): Promise<void>;
|
|
18
|
+
delete(key: string | string[]): Promise<void>;
|
|
19
|
+
clear(): Promise<void>;
|
|
20
|
+
|
|
21
|
+
// Batch operations
|
|
22
|
+
getMany<T>(keys: string[]): Promise<(T | null)[]>;
|
|
23
|
+
setMany<T>(entries: Array<{key: string, value: T, ttl?: number}>): Promise<void>;
|
|
24
|
+
deleteMany(keys: string[]): Promise<void>;
|
|
25
|
+
|
|
26
|
+
// Pattern-based operations
|
|
27
|
+
invalidatePattern(pattern: string): Promise<void>;
|
|
28
|
+
|
|
29
|
+
// Health check
|
|
30
|
+
ping(): Promise<boolean>;
|
|
31
|
+
|
|
32
|
+
// Statistics
|
|
33
|
+
getStats(): Promise<CacheStats>;
|
|
34
|
+
}
|