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/tests/setup.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global test setup file for BunSane
|
|
3
|
+
*
|
|
4
|
+
* This file is preloaded before all tests run (configured in bunfig.toml).
|
|
5
|
+
* It ensures:
|
|
6
|
+
* 1. Environment variables are loaded from .env.test
|
|
7
|
+
* 2. Database connection is established and ready
|
|
8
|
+
* 3. Base tables exist
|
|
9
|
+
* 4. ApplicationLifecycle is set to DATABASE_READY
|
|
10
|
+
* 5. EntityManager is ready for operations
|
|
11
|
+
* 6. Proper cleanup on exit
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { beforeAll, afterAll } from 'bun:test';
|
|
15
|
+
import { file } from 'bun';
|
|
16
|
+
|
|
17
|
+
// Load .env.test before anything else (skip when PGlite provides env vars)
|
|
18
|
+
if (process.env.USE_PGLITE !== 'true') {
|
|
19
|
+
const envTestPath = new URL('../.env.test', import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1');
|
|
20
|
+
const envFile = file(envTestPath);
|
|
21
|
+
if (await envFile.exists()) {
|
|
22
|
+
const envContent = await envFile.text();
|
|
23
|
+
for (const line of envContent.split('\n')) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
26
|
+
const [key, ...valueParts] = trimmed.split('=');
|
|
27
|
+
if (key) {
|
|
28
|
+
const value = valueParts.join('=');
|
|
29
|
+
process.env[key.trim()] = value.trim();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Suppress verbose logging during tests unless LOG_LEVEL is explicitly set
|
|
37
|
+
if (!process.env.LOG_LEVEL) {
|
|
38
|
+
process.env.LOG_LEVEL = 'warn';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Now import the rest after env is loaded
|
|
42
|
+
import db from '../database';
|
|
43
|
+
import { PrepareDatabase, HasValidBaseTable } from '../database/DatabaseHelper';
|
|
44
|
+
import ApplicationLifecycle, { ApplicationPhase } from '../core/ApplicationLifecycle';
|
|
45
|
+
import EntityManager from '../core/EntityManager';
|
|
46
|
+
import { ComponentRegistry } from '../core/components';
|
|
47
|
+
import { CacheManager } from '../core/cache';
|
|
48
|
+
import { logger } from '../core/Logger';
|
|
49
|
+
import { preparedStatementCache } from '../database/PreparedStatementCache';
|
|
50
|
+
|
|
51
|
+
let isSetupComplete = false;
|
|
52
|
+
let setupError: Error | null = null;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Initialize the test environment
|
|
56
|
+
*/
|
|
57
|
+
async function initializeTestEnvironment(): Promise<void> {
|
|
58
|
+
if (isSetupComplete) return;
|
|
59
|
+
if (setupError) throw setupError;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
logger.info({ scope: 'test-setup' }, 'Initializing test environment...');
|
|
63
|
+
|
|
64
|
+
// 1. Verify database connection by running a simple query
|
|
65
|
+
const connectionTest = await db`SELECT 1 as connected`;
|
|
66
|
+
if (!connectionTest || connectionTest.length === 0) {
|
|
67
|
+
throw new Error('Database connection failed');
|
|
68
|
+
}
|
|
69
|
+
logger.info({ scope: 'test-setup' }, 'Database connection verified');
|
|
70
|
+
|
|
71
|
+
// 2. Ensure base tables exist
|
|
72
|
+
const hasValidTables = await HasValidBaseTable();
|
|
73
|
+
if (!hasValidTables) {
|
|
74
|
+
logger.info({ scope: 'test-setup' }, 'Creating base database tables...');
|
|
75
|
+
await PrepareDatabase();
|
|
76
|
+
logger.info({ scope: 'test-setup' }, 'Base database tables created');
|
|
77
|
+
} else {
|
|
78
|
+
logger.info({ scope: 'test-setup' }, 'Base database tables already exist');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 3. Set ApplicationLifecycle to DATABASE_READY
|
|
82
|
+
ApplicationLifecycle.setPhase(ApplicationPhase.DATABASE_READY);
|
|
83
|
+
logger.info({ scope: 'test-setup' }, 'ApplicationLifecycle set to DATABASE_READY');
|
|
84
|
+
|
|
85
|
+
// 4. Set EntityManager as ready
|
|
86
|
+
(EntityManager as any).dbReady = true;
|
|
87
|
+
logger.info({ scope: 'test-setup' }, 'EntityManager marked as ready');
|
|
88
|
+
|
|
89
|
+
// 5. Initialize CacheManager with memory provider for tests
|
|
90
|
+
const cacheManager = CacheManager.getInstance();
|
|
91
|
+
await cacheManager.initialize({
|
|
92
|
+
enabled: true,
|
|
93
|
+
provider: 'memory',
|
|
94
|
+
strategy: 'write-through',
|
|
95
|
+
defaultTTL: 3600000,
|
|
96
|
+
entity: { enabled: true, ttl: 3600000 },
|
|
97
|
+
component: { enabled: true, ttl: 1800000 },
|
|
98
|
+
query: { enabled: false, ttl: 300000, maxSize: 10000 }
|
|
99
|
+
});
|
|
100
|
+
logger.info({ scope: 'test-setup' }, 'CacheManager initialized with memory provider');
|
|
101
|
+
|
|
102
|
+
// 6. Clear prepared statement cache to ensure clean slate
|
|
103
|
+
preparedStatementCache.clear();
|
|
104
|
+
|
|
105
|
+
isSetupComplete = true;
|
|
106
|
+
logger.info({ scope: 'test-setup' }, 'Test environment initialization complete');
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
setupError = error instanceof Error ? error : new Error(String(error));
|
|
110
|
+
logger.error({ scope: 'test-setup', error: setupError }, 'Failed to initialize test environment');
|
|
111
|
+
throw setupError;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Clean up the test environment
|
|
117
|
+
*/
|
|
118
|
+
async function cleanupTestEnvironment(): Promise<void> {
|
|
119
|
+
try {
|
|
120
|
+
logger.info({ scope: 'test-setup' }, 'Cleaning up test environment...');
|
|
121
|
+
|
|
122
|
+
// Clear caches
|
|
123
|
+
try {
|
|
124
|
+
const cacheManager = CacheManager.getInstance();
|
|
125
|
+
await cacheManager.clear();
|
|
126
|
+
} catch {
|
|
127
|
+
// Ignore cache cleanup errors
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Clear prepared statement cache
|
|
131
|
+
try {
|
|
132
|
+
preparedStatementCache.clear();
|
|
133
|
+
} catch {
|
|
134
|
+
// Ignore errors
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Note: We don't close the database connection pool here because
|
|
138
|
+
// Bun's test runner may still need it for parallel test files.
|
|
139
|
+
// The connection pool will be cleaned up when the process exits.
|
|
140
|
+
|
|
141
|
+
logger.info({ scope: 'test-setup' }, 'Test environment cleanup complete');
|
|
142
|
+
} catch (error) {
|
|
143
|
+
logger.warn({ scope: 'test-setup', error }, 'Error during test environment cleanup');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Register global hooks (skip for E2E tests that don't need DB)
|
|
148
|
+
if (process.env.SKIP_TEST_DB_SETUP !== 'true') {
|
|
149
|
+
beforeAll(async () => {
|
|
150
|
+
await initializeTestEnvironment();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
afterAll(async () => {
|
|
154
|
+
await cleanupTestEnvironment();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Export utilities for tests that need them
|
|
159
|
+
export { initializeTestEnvironment, cleanupTestEnvironment };
|
|
160
|
+
|
|
161
|
+
// Export a helper to ensure setup is complete (for tests that run before beforeAll)
|
|
162
|
+
export async function ensureTestSetup(): Promise<void> {
|
|
163
|
+
await initializeTestEnvironment();
|
|
164
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Benchmark execution engine for stress testing
|
|
3
|
+
* Runs queries with statistical analysis
|
|
4
|
+
*/
|
|
5
|
+
import db from '../../database';
|
|
6
|
+
|
|
7
|
+
export interface BenchmarkResult {
|
|
8
|
+
name: string;
|
|
9
|
+
iterations: number;
|
|
10
|
+
totalRecords: number;
|
|
11
|
+
timings: {
|
|
12
|
+
min: number;
|
|
13
|
+
max: number;
|
|
14
|
+
mean: number;
|
|
15
|
+
median: number;
|
|
16
|
+
p95: number;
|
|
17
|
+
p99: number;
|
|
18
|
+
stdDev: number;
|
|
19
|
+
};
|
|
20
|
+
rowsReturned: number;
|
|
21
|
+
queriesPerSecond: number;
|
|
22
|
+
memoryUsedMB: number;
|
|
23
|
+
passed: boolean;
|
|
24
|
+
target?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BenchmarkOptions {
|
|
28
|
+
iterations?: number;
|
|
29
|
+
warmupIterations?: number;
|
|
30
|
+
targetP95?: number;
|
|
31
|
+
collectMemory?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ConcurrentResult {
|
|
35
|
+
name: string;
|
|
36
|
+
concurrency: number;
|
|
37
|
+
totalQueries: number;
|
|
38
|
+
queriesPerSecond: number;
|
|
39
|
+
avgLatency: number;
|
|
40
|
+
errorRate: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class BenchmarkRunner {
|
|
44
|
+
private results: BenchmarkResult[] = [];
|
|
45
|
+
|
|
46
|
+
async run(
|
|
47
|
+
name: string,
|
|
48
|
+
queryFn: () => Promise<any[]>,
|
|
49
|
+
options: BenchmarkOptions = {}
|
|
50
|
+
): Promise<BenchmarkResult> {
|
|
51
|
+
const {
|
|
52
|
+
iterations = 20,
|
|
53
|
+
warmupIterations = 3,
|
|
54
|
+
targetP95,
|
|
55
|
+
collectMemory = true
|
|
56
|
+
} = options;
|
|
57
|
+
|
|
58
|
+
// Warmup phase
|
|
59
|
+
for (let i = 0; i < warmupIterations; i++) {
|
|
60
|
+
await queryFn();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Force GC if available
|
|
64
|
+
if (typeof global.gc === 'function') {
|
|
65
|
+
global.gc();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const times: number[] = [];
|
|
69
|
+
let rowCount = 0;
|
|
70
|
+
const memBefore = process.memoryUsage().heapUsed;
|
|
71
|
+
|
|
72
|
+
// Benchmark phase
|
|
73
|
+
for (let i = 0; i < iterations; i++) {
|
|
74
|
+
const start = performance.now();
|
|
75
|
+
const results = await queryFn();
|
|
76
|
+
times.push(performance.now() - start);
|
|
77
|
+
rowCount = Array.isArray(results) ? results.length : 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const memAfter = process.memoryUsage().heapUsed;
|
|
81
|
+
const sortedTimes = [...times].sort((a, b) => a - b);
|
|
82
|
+
|
|
83
|
+
const timings = {
|
|
84
|
+
min: sortedTimes[0] ?? 0,
|
|
85
|
+
max: sortedTimes[sortedTimes.length - 1] ?? 0,
|
|
86
|
+
mean: times.reduce((a, b) => a + b, 0) / (times.length || 1),
|
|
87
|
+
median: sortedTimes[Math.floor(sortedTimes.length / 2)] ?? 0,
|
|
88
|
+
p95: sortedTimes[Math.floor(sortedTimes.length * 0.95)] ?? sortedTimes[sortedTimes.length - 1] ?? 0,
|
|
89
|
+
p99: sortedTimes[Math.floor(sortedTimes.length * 0.99)] ?? sortedTimes[sortedTimes.length - 1] ?? 0,
|
|
90
|
+
stdDev: this.calculateStdDev(times)
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const result: BenchmarkResult = {
|
|
94
|
+
name,
|
|
95
|
+
iterations,
|
|
96
|
+
totalRecords: await this.getRecordCount(),
|
|
97
|
+
timings,
|
|
98
|
+
rowsReturned: rowCount,
|
|
99
|
+
queriesPerSecond: 1000 / (timings.mean || 1),
|
|
100
|
+
memoryUsedMB: collectMemory ? (memAfter - memBefore) / 1024 / 1024 : 0,
|
|
101
|
+
passed: targetP95 ? (timings.p95 ?? 0) <= targetP95 : true,
|
|
102
|
+
target: targetP95
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
this.results.push(result);
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async runConcurrent(
|
|
110
|
+
name: string,
|
|
111
|
+
queryFn: () => Promise<any[]>,
|
|
112
|
+
concurrency: number,
|
|
113
|
+
duration: number = 10000
|
|
114
|
+
): Promise<ConcurrentResult> {
|
|
115
|
+
const times: number[] = [];
|
|
116
|
+
let errors = 0;
|
|
117
|
+
const startTime = performance.now();
|
|
118
|
+
|
|
119
|
+
const worker = async () => {
|
|
120
|
+
while (performance.now() - startTime < duration) {
|
|
121
|
+
const queryStart = performance.now();
|
|
122
|
+
try {
|
|
123
|
+
await queryFn();
|
|
124
|
+
times.push(performance.now() - queryStart);
|
|
125
|
+
} catch {
|
|
126
|
+
errors++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
await Promise.all(Array(concurrency).fill(null).map(() => worker()));
|
|
132
|
+
|
|
133
|
+
const totalTime = performance.now() - startTime;
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
name,
|
|
137
|
+
concurrency,
|
|
138
|
+
totalQueries: times.length + errors,
|
|
139
|
+
queriesPerSecond: (times.length / totalTime) * 1000,
|
|
140
|
+
avgLatency: times.length > 0 ? times.reduce((a, b) => a + b, 0) / times.length : 0,
|
|
141
|
+
errorRate: (times.length + errors) > 0 ? errors / (times.length + errors) : 0
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run a benchmark and print detailed results
|
|
147
|
+
*/
|
|
148
|
+
async runWithOutput(
|
|
149
|
+
name: string,
|
|
150
|
+
queryFn: () => Promise<any[]>,
|
|
151
|
+
options: BenchmarkOptions = {}
|
|
152
|
+
): Promise<BenchmarkResult> {
|
|
153
|
+
console.log(` Running: ${name}...`);
|
|
154
|
+
const result = await this.run(name, queryFn, options);
|
|
155
|
+
|
|
156
|
+
const status = result.passed ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
|
|
157
|
+
console.log(
|
|
158
|
+
` ${status} p50=${result.timings.median.toFixed(1)}ms ` +
|
|
159
|
+
`p95=${result.timings.p95.toFixed(1)}ms ` +
|
|
160
|
+
`rows=${result.rowsReturned} ` +
|
|
161
|
+
`QPS=${result.queriesPerSecond.toFixed(0)}`
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (!result.passed && result.target) {
|
|
165
|
+
console.log(` Target: p95 <= ${result.target}ms`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private calculateStdDev(values: number[]): number {
|
|
172
|
+
if (values.length === 0) return 0;
|
|
173
|
+
const mean = values.reduce((a, b) => a + b) / values.length;
|
|
174
|
+
const squareDiffs = values.map(value => Math.pow(value - mean, 2));
|
|
175
|
+
return Math.sqrt(squareDiffs.reduce((a, b) => a + b) / values.length);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private async getRecordCount(): Promise<number> {
|
|
179
|
+
try {
|
|
180
|
+
const result = await db`SELECT COUNT(*) as count FROM entities WHERE deleted_at IS NULL`;
|
|
181
|
+
return parseInt(result[0].count);
|
|
182
|
+
} catch {
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
getResults(): BenchmarkResult[] {
|
|
188
|
+
return [...this.results];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
clearResults(): void {
|
|
192
|
+
this.results = [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get summary statistics
|
|
197
|
+
*/
|
|
198
|
+
getSummary(): { passed: number; failed: number; total: number } {
|
|
199
|
+
const passed = this.results.filter(r => r.passed).length;
|
|
200
|
+
const failed = this.results.filter(r => !r.passed).length;
|
|
201
|
+
return { passed, failed, total: this.results.length };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data seeder for stress testing
|
|
3
|
+
* Performs optimized bulk inserts for maximum throughput
|
|
4
|
+
*/
|
|
5
|
+
import db from '../../database';
|
|
6
|
+
import { sql } from 'bun';
|
|
7
|
+
import { ComponentRegistry } from '../../core/components';
|
|
8
|
+
import { getMetadataStorage } from '../../core/metadata';
|
|
9
|
+
import { uuidv7 } from '../../utils/uuid';
|
|
10
|
+
import type { BaseComponent } from '../../core/components/BaseComponent';
|
|
11
|
+
|
|
12
|
+
export interface SeederOptions {
|
|
13
|
+
totalEntities: number;
|
|
14
|
+
batchSize: number;
|
|
15
|
+
onProgress?: (current: number, total: number, elapsedMs: number) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SeederResult {
|
|
19
|
+
entityIds: string[];
|
|
20
|
+
totalTime: number;
|
|
21
|
+
recordsPerSecond: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type ComponentConstructor = new () => BaseComponent;
|
|
25
|
+
|
|
26
|
+
export class DataSeeder {
|
|
27
|
+
/**
|
|
28
|
+
* Seeds the database with test entities and components
|
|
29
|
+
* Uses optimized bulk inserts for maximum throughput
|
|
30
|
+
*/
|
|
31
|
+
async seed<T extends BaseComponent>(
|
|
32
|
+
componentClass: ComponentConstructor,
|
|
33
|
+
dataGenerator: (index: number) => Record<string, any>,
|
|
34
|
+
options: SeederOptions
|
|
35
|
+
): Promise<SeederResult> {
|
|
36
|
+
const { totalEntities, batchSize, onProgress } = options;
|
|
37
|
+
const entityIds: string[] = [];
|
|
38
|
+
const startTime = performance.now();
|
|
39
|
+
|
|
40
|
+
// Ensure component is registered (just wait for readiness, don't trigger registration)
|
|
41
|
+
const componentName = componentClass.name;
|
|
42
|
+
await ComponentRegistry.getReadyPromise(componentName);
|
|
43
|
+
|
|
44
|
+
const storage = getMetadataStorage();
|
|
45
|
+
const typeId = storage.getComponentId(componentName);
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < totalEntities; i += batchSize) {
|
|
48
|
+
const currentBatch = Math.min(batchSize, totalEntities - i);
|
|
49
|
+
const now = new Date();
|
|
50
|
+
|
|
51
|
+
// Build batch data arrays
|
|
52
|
+
const entitiesToInsert: { id: string; created_at: Date; updated_at: Date }[] = [];
|
|
53
|
+
const componentsToInsert: { id: string; entity_id: string; type_id: string; name: string; data: any; created_at: Date; updated_at: Date }[] = [];
|
|
54
|
+
const entityComponentsToInsert: { entity_id: string; type_id: string; component_id: string; created_at: Date; updated_at: Date }[] = [];
|
|
55
|
+
|
|
56
|
+
// Generate batch data
|
|
57
|
+
for (let j = 0; j < currentBatch; j++) {
|
|
58
|
+
const entityId = uuidv7();
|
|
59
|
+
const componentId = uuidv7();
|
|
60
|
+
const data = dataGenerator(i + j);
|
|
61
|
+
|
|
62
|
+
entitiesToInsert.push({
|
|
63
|
+
id: entityId,
|
|
64
|
+
created_at: now,
|
|
65
|
+
updated_at: now
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
componentsToInsert.push({
|
|
69
|
+
id: componentId,
|
|
70
|
+
entity_id: entityId,
|
|
71
|
+
type_id: typeId,
|
|
72
|
+
name: componentName,
|
|
73
|
+
data: data,
|
|
74
|
+
created_at: now,
|
|
75
|
+
updated_at: now
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
entityComponentsToInsert.push({
|
|
79
|
+
entity_id: entityId,
|
|
80
|
+
type_id: typeId,
|
|
81
|
+
component_id: componentId,
|
|
82
|
+
created_at: now,
|
|
83
|
+
updated_at: now
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
entityIds.push(entityId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Bulk insert entities using Bun's sql helper
|
|
90
|
+
await db`INSERT INTO entities ${sql(entitiesToInsert, 'id', 'created_at', 'updated_at')}`;
|
|
91
|
+
|
|
92
|
+
// Bulk insert components
|
|
93
|
+
await db`INSERT INTO components ${sql(componentsToInsert, 'id', 'entity_id', 'type_id', 'name', 'data', 'created_at', 'updated_at')}`;
|
|
94
|
+
|
|
95
|
+
// Bulk insert entity_components index
|
|
96
|
+
await db`INSERT INTO entity_components ${sql(entityComponentsToInsert, 'entity_id', 'type_id', 'component_id', 'created_at', 'updated_at')} ON CONFLICT (entity_id, type_id) DO NOTHING`;
|
|
97
|
+
|
|
98
|
+
if (onProgress) {
|
|
99
|
+
onProgress(i + currentBatch, totalEntities, performance.now() - startTime);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const totalTime = performance.now() - startTime;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
entityIds,
|
|
107
|
+
totalTime,
|
|
108
|
+
recordsPerSecond: (totalEntities / totalTime) * 1000
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Seeds multiple components for existing entities
|
|
114
|
+
*/
|
|
115
|
+
async seedAdditionalComponent<T extends BaseComponent>(
|
|
116
|
+
entityIds: string[],
|
|
117
|
+
componentClass: ComponentConstructor,
|
|
118
|
+
dataGenerator: (index: number, entityId: string) => Record<string, any>,
|
|
119
|
+
batchSize: number = 5000
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
const componentName = componentClass.name;
|
|
122
|
+
await ComponentRegistry.getReadyPromise(componentName);
|
|
123
|
+
|
|
124
|
+
const storage = getMetadataStorage();
|
|
125
|
+
const typeId = storage.getComponentId(componentName);
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < entityIds.length; i += batchSize) {
|
|
128
|
+
const batchEntityIds = entityIds.slice(i, i + batchSize);
|
|
129
|
+
const now = new Date();
|
|
130
|
+
|
|
131
|
+
const componentsToInsert: { id: string; entity_id: string; type_id: string; name: string; data: any; created_at: Date; updated_at: Date }[] = [];
|
|
132
|
+
const entityComponentsToInsert: { entity_id: string; type_id: string; component_id: string; created_at: Date; updated_at: Date }[] = [];
|
|
133
|
+
|
|
134
|
+
for (let j = 0; j < batchEntityIds.length; j++) {
|
|
135
|
+
const entityId = batchEntityIds[j]!;
|
|
136
|
+
const componentId = uuidv7();
|
|
137
|
+
const data = dataGenerator(i + j, entityId);
|
|
138
|
+
|
|
139
|
+
componentsToInsert.push({
|
|
140
|
+
id: componentId,
|
|
141
|
+
entity_id: entityId,
|
|
142
|
+
type_id: typeId,
|
|
143
|
+
name: componentName,
|
|
144
|
+
data: data,
|
|
145
|
+
created_at: now,
|
|
146
|
+
updated_at: now
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
entityComponentsToInsert.push({
|
|
150
|
+
entity_id: entityId,
|
|
151
|
+
type_id: typeId,
|
|
152
|
+
component_id: componentId,
|
|
153
|
+
created_at: now,
|
|
154
|
+
updated_at: now
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await db`INSERT INTO components ${sql(componentsToInsert, 'id', 'entity_id', 'type_id', 'name', 'data', 'created_at', 'updated_at')}`;
|
|
159
|
+
await db`INSERT INTO entity_components ${sql(entityComponentsToInsert, 'entity_id', 'type_id', 'component_id', 'created_at', 'updated_at')} ON CONFLICT (entity_id, type_id) DO NOTHING`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Cleans up seeded data
|
|
165
|
+
*/
|
|
166
|
+
async cleanup(entityIds: string[], batchSize: number = 10000): Promise<void> {
|
|
167
|
+
for (let i = 0; i < entityIds.length; i += batchSize) {
|
|
168
|
+
const batch = entityIds.slice(i, i + batchSize);
|
|
169
|
+
// Use individual deletes for reliability
|
|
170
|
+
await db`DELETE FROM entities WHERE id IN ${sql(batch.map(id => [id]))}`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Runs VACUUM ANALYZE for optimal query planning
|
|
176
|
+
*/
|
|
177
|
+
async optimize(): Promise<void> {
|
|
178
|
+
await db.unsafe('VACUUM ANALYZE entities');
|
|
179
|
+
await db.unsafe('VACUUM ANALYZE components');
|
|
180
|
+
await db.unsafe('VACUUM ANALYZE entity_components');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Gets the current record count
|
|
185
|
+
*/
|
|
186
|
+
async getRecordCount(): Promise<number> {
|
|
187
|
+
const result = await db`SELECT COUNT(*) as count FROM entities WHERE deleted_at IS NULL`;
|
|
188
|
+
return parseInt(result[0].count);
|
|
189
|
+
}
|
|
190
|
+
}
|