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
package/core/BatchLoader.ts
CHANGED
|
@@ -1,100 +1,210 @@
|
|
|
1
|
-
import { Entity } from "
|
|
2
|
-
import { BaseComponent } from "
|
|
1
|
+
import { Entity } from "./Entity";
|
|
2
|
+
import { BaseComponent } from "./components";
|
|
3
3
|
import { timed } from "./Decorators";
|
|
4
4
|
import db from "../database";
|
|
5
5
|
import { sql } from "bun";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
private maxPoolSize = 1000;
|
|
12
|
-
|
|
13
|
-
static getInstance(): EntityPool {
|
|
14
|
-
if (!EntityPool.instance) {
|
|
15
|
-
EntityPool.instance = new EntityPool();
|
|
16
|
-
}
|
|
17
|
-
return EntityPool.instance;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
get(entityId: string): Entity | null {
|
|
21
|
-
const entities = this.pool.get(entityId);
|
|
22
|
-
if (entities && entities.length > 0) {
|
|
23
|
-
return entities.pop()!;
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
put(entity: Entity): void {
|
|
29
|
-
const entityId = entity.id;
|
|
30
|
-
let entities = this.pool.get(entityId);
|
|
31
|
-
if (!entities) {
|
|
32
|
-
entities = [];
|
|
33
|
-
this.pool.set(entityId, entities);
|
|
34
|
-
}
|
|
35
|
-
if (entities.length < this.maxPoolSize) {
|
|
36
|
-
entities.push(entity);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
7
|
+
interface CachedRelation {
|
|
8
|
+
ids: string[];
|
|
9
|
+
expiresAt: number;
|
|
10
|
+
}
|
|
39
11
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
12
|
+
interface BatchLoaderOptions {
|
|
13
|
+
fieldName?: string;
|
|
14
|
+
batchSize?: number;
|
|
15
|
+
maxConcurrency?: number;
|
|
16
|
+
cacheTTL?: number;
|
|
43
17
|
}
|
|
44
18
|
|
|
45
19
|
export class BatchLoader {
|
|
46
|
-
private static
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
static
|
|
50
|
-
entities: Entity[],
|
|
51
|
-
component: new () => C,
|
|
52
|
-
loader: (ids: string[]) => Promise<Entity[]>
|
|
53
|
-
): Promise<Map<string, Entity>> {
|
|
54
|
-
const ids: string[] = [];
|
|
55
|
-
for (const entity of entities) {
|
|
56
|
-
const data = await entity.get(component) as any;
|
|
57
|
-
if (data?.value) {
|
|
58
|
-
ids.push(data.value);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
const uniqueIds = [...new Set(ids)];
|
|
62
|
-
const relatedEntities = await loader(uniqueIds);
|
|
63
|
-
const map = new Map<string, Entity>();
|
|
64
|
-
for (const related of relatedEntities) {
|
|
65
|
-
map.set(related.id, related);
|
|
66
|
-
}
|
|
67
|
-
return map;
|
|
68
|
-
}
|
|
20
|
+
private static cache = new Map<string, Map<string, CachedRelation>>();
|
|
21
|
+
private static readonly DEFAULT_BATCH_SIZE = 1000;
|
|
22
|
+
private static readonly DEFAULT_MAX_CONCURRENCY = 5;
|
|
23
|
+
private static readonly DEFAULT_CACHE_TTL = 300_000; // 5 minutes
|
|
69
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Load related entities efficiently with caching and batching
|
|
27
|
+
*/
|
|
70
28
|
@timed("BatchLoader.loadRelatedEntitiesBatched")
|
|
71
29
|
static async loadRelatedEntitiesBatched<C extends BaseComponent>(
|
|
72
30
|
entities: Entity[],
|
|
73
31
|
component: new () => C,
|
|
74
|
-
loader: (ids: string[]) => Promise<Entity[]
|
|
32
|
+
loader: (ids: string[]) => Promise<Entity[]>,
|
|
33
|
+
options: BatchLoaderOptions = {}
|
|
75
34
|
): Promise<Map<string, Entity>> {
|
|
76
35
|
if (entities.length === 0) return new Map();
|
|
77
36
|
|
|
37
|
+
const {
|
|
38
|
+
fieldName = 'value',
|
|
39
|
+
batchSize = this.DEFAULT_BATCH_SIZE,
|
|
40
|
+
maxConcurrency = this.DEFAULT_MAX_CONCURRENCY,
|
|
41
|
+
cacheTTL = this.DEFAULT_CACHE_TTL
|
|
42
|
+
} = options;
|
|
43
|
+
|
|
78
44
|
const comp = new component();
|
|
79
45
|
const typeId = comp.getTypeID();
|
|
80
46
|
const parentIds = entities.map(e => e.id);
|
|
81
47
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
48
|
+
// Check cache first
|
|
49
|
+
const cacheKey = `${typeId}:${fieldName}`;
|
|
50
|
+
let cachedResults = this.cache.get(cacheKey);
|
|
51
|
+
if (!cachedResults) {
|
|
52
|
+
cachedResults = new Map();
|
|
53
|
+
this.cache.set(cacheKey, cachedResults);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
|
|
58
|
+
// Get uncached or expired parent IDs
|
|
59
|
+
const uncachedParentIds = parentIds.filter(id => {
|
|
60
|
+
const entry = cachedResults.get(id);
|
|
61
|
+
if (!entry) return true;
|
|
62
|
+
if (now > entry.expiresAt) {
|
|
63
|
+
cachedResults.delete(id);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (uncachedParentIds.length > 0) {
|
|
70
|
+
// Batch the parent IDs to avoid huge IN clauses
|
|
71
|
+
const batches = this.chunkArray(uncachedParentIds, batchSize);
|
|
72
|
+
|
|
73
|
+
for (const batch of batches) {
|
|
74
|
+
const rows = await db`
|
|
75
|
+
SELECT c.entity_id, (c.data->>${sql(fieldName)}) AS related_id
|
|
76
|
+
FROM components c
|
|
77
|
+
WHERE c.entity_id IN ${sql(batch)}
|
|
78
|
+
AND c.type_id = ${typeId}
|
|
79
|
+
AND c.deleted_at IS NULL
|
|
80
|
+
AND c.data->>${sql(fieldName)} IS NOT NULL
|
|
81
|
+
`;
|
|
89
82
|
|
|
90
|
-
|
|
91
|
-
|
|
83
|
+
// Group by parent entity for caching
|
|
84
|
+
const parentGroups = new Map<string, string[]>();
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
const parentId = row.entity_id;
|
|
87
|
+
const relatedId = row.related_id;
|
|
88
|
+
if (!parentGroups.has(parentId)) {
|
|
89
|
+
parentGroups.set(parentId, []);
|
|
90
|
+
}
|
|
91
|
+
parentGroups.get(parentId)!.push(relatedId);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const expiresAt = Date.now() + cacheTTL;
|
|
95
|
+
|
|
96
|
+
// Cache the related IDs for each parent
|
|
97
|
+
for (const [parentId, relatedIds] of parentGroups) {
|
|
98
|
+
cachedResults.set(parentId, { ids: relatedIds, expiresAt });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Cache empty arrays for parents with no relations
|
|
102
|
+
for (const parentId of batch) {
|
|
103
|
+
if (!parentGroups.has(parentId)) {
|
|
104
|
+
cachedResults.set(parentId, { ids: [], expiresAt });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Collect all unique related IDs from cache
|
|
111
|
+
const allRelatedIds = new Set<string>();
|
|
112
|
+
for (const parentId of parentIds) {
|
|
113
|
+
const entry = cachedResults.get(parentId);
|
|
114
|
+
const relatedIds = entry?.ids || [];
|
|
115
|
+
relatedIds.forEach((id: string) => allRelatedIds.add(id));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (allRelatedIds.size === 0) return new Map();
|
|
119
|
+
|
|
120
|
+
// Load related entities with concurrency control
|
|
121
|
+
const relatedIdArray = Array.from(allRelatedIds);
|
|
122
|
+
const entityBatches = this.chunkArray(relatedIdArray, batchSize);
|
|
123
|
+
|
|
124
|
+
const relatedEntities: Entity[] = [];
|
|
125
|
+
const semaphore = new Semaphore(maxConcurrency);
|
|
126
|
+
|
|
127
|
+
await Promise.all(
|
|
128
|
+
entityBatches.map(async (batch) => {
|
|
129
|
+
await semaphore.acquire();
|
|
130
|
+
try {
|
|
131
|
+
const entities = await loader(batch);
|
|
132
|
+
relatedEntities.push(...entities);
|
|
133
|
+
} finally {
|
|
134
|
+
semaphore.release();
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
);
|
|
92
138
|
|
|
93
|
-
const relatedEntities = await loader(uniqueIds);
|
|
94
139
|
const map = new Map<string, Entity>();
|
|
95
140
|
for (const related of relatedEntities) {
|
|
96
141
|
map.set(related.id, related);
|
|
97
142
|
}
|
|
98
143
|
return map;
|
|
99
144
|
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Clear the cache (useful for testing or memory management)
|
|
148
|
+
*/
|
|
149
|
+
static clearCache(): void {
|
|
150
|
+
this.cache.clear();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get cache statistics including expired entry count
|
|
155
|
+
*/
|
|
156
|
+
static getCacheStats(): { size: number; entries: number; expired: number } {
|
|
157
|
+
let totalEntries = 0;
|
|
158
|
+
let expiredEntries = 0;
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
for (const [, parentMap] of this.cache) {
|
|
161
|
+
for (const [, entry] of parentMap) {
|
|
162
|
+
totalEntries++;
|
|
163
|
+
if (now > entry.expiresAt) {
|
|
164
|
+
expiredEntries++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { size: this.cache.size, entries: totalEntries, expired: expiredEntries };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private static chunkArray<T>(array: T[], size: number): T[][] {
|
|
172
|
+
const chunks: T[][] = [];
|
|
173
|
+
for (let i = 0; i < array.length; i += size) {
|
|
174
|
+
chunks.push(array.slice(i, i + size));
|
|
175
|
+
}
|
|
176
|
+
return chunks;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Simple semaphore for concurrency control
|
|
182
|
+
*/
|
|
183
|
+
class Semaphore {
|
|
184
|
+
private permits: number;
|
|
185
|
+
private waitQueue: (() => void)[] = [];
|
|
186
|
+
|
|
187
|
+
constructor(permits: number) {
|
|
188
|
+
this.permits = permits;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async acquire(): Promise<void> {
|
|
192
|
+
if (this.permits > 0) {
|
|
193
|
+
this.permits--;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return new Promise((resolve) => {
|
|
198
|
+
this.waitQueue.push(resolve);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
release(): void {
|
|
203
|
+
this.permits++;
|
|
204
|
+
if (this.waitQueue.length > 0) {
|
|
205
|
+
const resolve = this.waitQueue.shift()!;
|
|
206
|
+
this.permits--;
|
|
207
|
+
resolve();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
100
210
|
}
|
package/core/Config.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration module for Bunsane Framework
|
|
3
|
+
* Handles environment variables and application settings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface BunsaneConfig {
|
|
7
|
+
// Query optimization settings
|
|
8
|
+
useLateralJoins: boolean;
|
|
9
|
+
partitionStrategy: 'list' | 'hash';
|
|
10
|
+
useDirectPartition: boolean;
|
|
11
|
+
|
|
12
|
+
// Database settings
|
|
13
|
+
databaseUrl?: string;
|
|
14
|
+
databasePoolSize?: number;
|
|
15
|
+
|
|
16
|
+
// Application settings
|
|
17
|
+
appPort: number;
|
|
18
|
+
nodeEnv: string;
|
|
19
|
+
|
|
20
|
+
// Debug settings
|
|
21
|
+
debugMode: boolean;
|
|
22
|
+
logLevel: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Default configuration values
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_CONFIG: BunsaneConfig = {
|
|
29
|
+
useLateralJoins: true, // Default to true for PG12+
|
|
30
|
+
partitionStrategy: 'list', // LIST partitioning - one partition per component type
|
|
31
|
+
useDirectPartition: true, // Direct partition access - queries go directly to partition tables
|
|
32
|
+
appPort: 3000,
|
|
33
|
+
nodeEnv: process.env.NODE_ENV || 'development',
|
|
34
|
+
debugMode: false,
|
|
35
|
+
logLevel: 'info'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Configuration singleton class
|
|
40
|
+
*/
|
|
41
|
+
class ConfigManager {
|
|
42
|
+
private static instance: ConfigManager;
|
|
43
|
+
private config: BunsaneConfig;
|
|
44
|
+
|
|
45
|
+
private constructor() {
|
|
46
|
+
this.config = this.loadConfig();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public static getInstance(): ConfigManager {
|
|
50
|
+
if (!ConfigManager.instance) {
|
|
51
|
+
ConfigManager.instance = new ConfigManager();
|
|
52
|
+
}
|
|
53
|
+
return ConfigManager.instance;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load configuration from environment variables
|
|
58
|
+
*/
|
|
59
|
+
private loadConfig(): BunsaneConfig {
|
|
60
|
+
return {
|
|
61
|
+
useLateralJoins: this.parseBoolean(process.env.BUNSANE_USE_LATERAL_JOINS, DEFAULT_CONFIG.useLateralJoins),
|
|
62
|
+
partitionStrategy: this.parsePartitionStrategy(process.env.BUNSANE_PARTITION_STRATEGY, DEFAULT_CONFIG.partitionStrategy),
|
|
63
|
+
useDirectPartition: this.parseBoolean(process.env.BUNSANE_USE_DIRECT_PARTITION, DEFAULT_CONFIG.useDirectPartition),
|
|
64
|
+
databaseUrl: process.env.DATABASE_URL,
|
|
65
|
+
databasePoolSize: this.parseNumber(process.env.DATABASE_POOL_SIZE, 10),
|
|
66
|
+
appPort: this.parseNumber(process.env.APP_PORT, DEFAULT_CONFIG.appPort),
|
|
67
|
+
nodeEnv: process.env.NODE_ENV || DEFAULT_CONFIG.nodeEnv,
|
|
68
|
+
debugMode: this.parseBoolean(process.env.DEBUG, DEFAULT_CONFIG.debugMode),
|
|
69
|
+
logLevel: process.env.LOG_LEVEL || DEFAULT_CONFIG.logLevel
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private parseBoolean(value: string | undefined, defaultValue: boolean): boolean {
|
|
74
|
+
if (!value) return defaultValue;
|
|
75
|
+
return value.toLowerCase() === 'true' || value === '1';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private parseNumber(value: string | undefined, defaultValue: number): number {
|
|
79
|
+
if (!value) return defaultValue;
|
|
80
|
+
const parsed = parseInt(value, 10);
|
|
81
|
+
return isNaN(parsed) ? defaultValue : parsed;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private parsePartitionStrategy(value: string | undefined, defaultValue: 'list' | 'hash'): 'list' | 'hash' {
|
|
85
|
+
if (!value) return defaultValue;
|
|
86
|
+
const lowerValue = value.toLowerCase();
|
|
87
|
+
return lowerValue === 'hash' ? 'hash' : 'list';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get the current configuration
|
|
92
|
+
*/
|
|
93
|
+
public getConfig(): BunsaneConfig {
|
|
94
|
+
return { ...this.config };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get a specific configuration value
|
|
99
|
+
*/
|
|
100
|
+
public get<K extends keyof BunsaneConfig>(key: K): BunsaneConfig[K] {
|
|
101
|
+
return this.config[key];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if LATERAL joins should be used
|
|
106
|
+
*/
|
|
107
|
+
public shouldUseLateralJoins(): boolean {
|
|
108
|
+
return this.config.useLateralJoins;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if debug mode is enabled
|
|
113
|
+
*/
|
|
114
|
+
public isDebugMode(): boolean {
|
|
115
|
+
return this.config.debugMode;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get partition strategy
|
|
120
|
+
*/
|
|
121
|
+
public getPartitionStrategy(): 'list' | 'hash' {
|
|
122
|
+
return this.config.partitionStrategy;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if direct partition table access should be used
|
|
127
|
+
*/
|
|
128
|
+
public shouldUseDirectPartition(): boolean {
|
|
129
|
+
return this.config.useDirectPartition;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Reload configuration from environment variables
|
|
134
|
+
*/
|
|
135
|
+
public reloadConfig(): void {
|
|
136
|
+
this.config = this.loadConfig();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Global configuration instance
|
|
142
|
+
*/
|
|
143
|
+
export const config = ConfigManager.getInstance();
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Convenience functions for common config checks
|
|
147
|
+
*/
|
|
148
|
+
export const shouldUseLateralJoins = () => config.shouldUseLateralJoins();
|
|
149
|
+
export const isDebugMode = () => config.isDebugMode();
|
|
150
|
+
export const getPartitionStrategy = () => config.getPartitionStrategy();
|
|
151
|
+
export const shouldUseDirectPartition = () => config.shouldUseDirectPartition();
|
|
152
|
+
|
|
153
|
+
export default config;
|
package/core/Decorators.ts
CHANGED
|
@@ -16,7 +16,10 @@ export function timed(hint?: string) {
|
|
|
16
16
|
const start = performance.now();
|
|
17
17
|
const result = await originalMethod.apply(this, args);
|
|
18
18
|
const end = performance.now();
|
|
19
|
-
|
|
19
|
+
if(end - start > 100) {
|
|
20
|
+
logger.warn(`Execution time for ${propertyKey}${hint ? ` (${hint})` : ''}: ${end - start} ms`);
|
|
21
|
+
}
|
|
22
|
+
// logger.trace(`Execution time for ${propertyKey}${hint ? ` (${hint})` : ''}: ${end - start} ms`);
|
|
20
23
|
return result;
|
|
21
24
|
} else {
|
|
22
25
|
return await originalMethod.apply(this, args);
|