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.
Files changed (257) hide show
  1. package/.claude/settings.local.json +47 -0
  2. package/.claude/skills/update-memory.md +74 -0
  3. package/.prettierrc +4 -0
  4. package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
  5. package/.serena/memories/architecture.md +154 -0
  6. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
  7. package/.serena/memories/code_style_and_conventions.md +76 -0
  8. package/.serena/memories/project_overview.md +43 -0
  9. package/.serena/memories/schema-dsl-plan.md +107 -0
  10. package/.serena/memories/suggested_commands.md +80 -0
  11. package/.serena/memories/typescript-compilation-status.md +54 -0
  12. package/.serena/project.yml +114 -0
  13. package/TODO.md +1 -7
  14. package/bun.lock +150 -4
  15. package/bunfig.toml +10 -0
  16. package/config/cache.config.ts +77 -0
  17. package/config/upload.config.ts +4 -5
  18. package/core/App.ts +870 -123
  19. package/core/ArcheType.ts +2268 -377
  20. package/core/BatchLoader.ts +181 -71
  21. package/core/Config.ts +153 -0
  22. package/core/Decorators.ts +4 -1
  23. package/core/Entity.ts +621 -92
  24. package/core/EntityHookManager.ts +1 -1
  25. package/core/EntityInterface.ts +3 -1
  26. package/core/EntityManager.ts +1 -13
  27. package/core/ErrorHandler.ts +8 -2
  28. package/core/Logger.ts +9 -0
  29. package/core/Middleware.ts +34 -0
  30. package/core/RequestContext.ts +5 -1
  31. package/core/RequestLoaders.ts +227 -93
  32. package/core/SchedulerManager.ts +193 -52
  33. package/core/cache/CacheAnalytics.ts +399 -0
  34. package/core/cache/CacheFactory.ts +145 -0
  35. package/core/cache/CacheManager.ts +520 -0
  36. package/core/cache/CacheProvider.ts +34 -0
  37. package/core/cache/CacheWarmer.ts +157 -0
  38. package/core/cache/CompressionUtils.ts +110 -0
  39. package/core/cache/MemoryCache.ts +251 -0
  40. package/core/cache/MultiLevelCache.ts +180 -0
  41. package/core/cache/NoOpCache.ts +53 -0
  42. package/core/cache/RedisCache.ts +464 -0
  43. package/core/cache/TTLStrategy.ts +254 -0
  44. package/core/cache/index.ts +6 -0
  45. package/core/components/BaseComponent.ts +120 -0
  46. package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
  47. package/core/components/Decorators.ts +88 -0
  48. package/core/components/Interfaces.ts +7 -0
  49. package/core/components/index.ts +5 -0
  50. package/core/decorators/EntityHooks.ts +0 -3
  51. package/core/decorators/IndexedField.ts +26 -0
  52. package/core/decorators/ScheduledTask.ts +0 -47
  53. package/core/events/EntityLifecycleEvents.ts +1 -1
  54. package/core/health.ts +112 -0
  55. package/core/metadata/definitions/ArcheType.ts +14 -0
  56. package/core/metadata/definitions/Component.ts +9 -0
  57. package/core/metadata/definitions/gqlObject.ts +1 -1
  58. package/core/metadata/index.ts +42 -1
  59. package/core/metadata/metadata-storage.ts +28 -2
  60. package/core/middleware/AccessLog.ts +59 -0
  61. package/core/middleware/RequestId.ts +38 -0
  62. package/core/middleware/SecurityHeaders.ts +62 -0
  63. package/core/middleware/index.ts +3 -0
  64. package/core/scheduler/DistributedLock.ts +266 -0
  65. package/core/scheduler/index.ts +15 -0
  66. package/core/validateEnv.ts +92 -0
  67. package/database/DatabaseHelper.ts +416 -40
  68. package/database/IndexingStrategy.ts +342 -0
  69. package/database/PreparedStatementCache.ts +226 -0
  70. package/database/index.ts +32 -7
  71. package/database/sqlHelpers.ts +14 -2
  72. package/endpoints/archetypes.ts +362 -0
  73. package/endpoints/components.ts +58 -0
  74. package/endpoints/entity.ts +80 -0
  75. package/endpoints/index.ts +27 -0
  76. package/endpoints/query.ts +93 -0
  77. package/endpoints/stats.ts +76 -0
  78. package/endpoints/tables.ts +212 -0
  79. package/endpoints/types.ts +155 -0
  80. package/gql/ArchetypeOperations.ts +32 -86
  81. package/gql/Generator.ts +27 -315
  82. package/gql/GeneratorV2.ts +37 -0
  83. package/gql/builders/InputTypeBuilder.ts +99 -0
  84. package/gql/builders/ResolverBuilder.ts +234 -0
  85. package/gql/builders/TypeDefBuilder.ts +105 -0
  86. package/gql/builders/index.ts +3 -0
  87. package/gql/decorators/Upload.ts +1 -1
  88. package/gql/depthLimit.ts +85 -0
  89. package/gql/graph/GraphNode.ts +224 -0
  90. package/gql/graph/SchemaGraph.ts +278 -0
  91. package/gql/helpers.ts +8 -2
  92. package/gql/index.ts +56 -4
  93. package/gql/middleware.ts +79 -0
  94. package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
  95. package/gql/orchestration/index.ts +1 -0
  96. package/gql/scanner/ServiceScanner.ts +347 -0
  97. package/gql/schema/index.ts +458 -0
  98. package/gql/strategies/TypeGenerationStrategy.ts +329 -0
  99. package/gql/types.ts +1 -0
  100. package/gql/utils/TypeSignature.ts +220 -0
  101. package/gql/utils/index.ts +1 -0
  102. package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
  103. package/gql/visitors/DeduplicationVisitor.ts +82 -0
  104. package/gql/visitors/GraphVisitor.ts +78 -0
  105. package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
  106. package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
  107. package/gql/visitors/TypeCollectorVisitor.ts +79 -0
  108. package/gql/visitors/VisitorComposer.ts +96 -0
  109. package/gql/visitors/index.ts +7 -0
  110. package/package.json +59 -37
  111. package/plugins/index.ts +2 -2
  112. package/query/CTENode.ts +97 -0
  113. package/query/ComponentInclusionNode.ts +689 -0
  114. package/query/FilterBuilder.ts +127 -0
  115. package/query/FilterBuilderRegistry.ts +202 -0
  116. package/query/OrNode.ts +517 -0
  117. package/query/OrQuery.ts +42 -0
  118. package/query/Query.ts +1022 -0
  119. package/query/QueryContext.ts +170 -0
  120. package/query/QueryDAG.ts +122 -0
  121. package/query/QueryNode.ts +65 -0
  122. package/query/SourceNode.ts +53 -0
  123. package/query/builders/FullTextSearchBuilder.ts +236 -0
  124. package/query/index.ts +21 -0
  125. package/scheduler/index.ts +40 -8
  126. package/service/Service.ts +2 -1
  127. package/service/ServiceRegistry.ts +6 -5
  128. package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
  129. package/storage/S3StorageProvider.ts +316 -0
  130. package/{core/storage → storage}/StorageProvider.ts +7 -3
  131. package/studio/bun.lock +482 -0
  132. package/studio/index.html +13 -0
  133. package/studio/package.json +39 -0
  134. package/studio/postcss.config.js +6 -0
  135. package/studio/src/components/DataTable.tsx +211 -0
  136. package/studio/src/components/Layout.tsx +13 -0
  137. package/studio/src/components/PageContainer.tsx +9 -0
  138. package/studio/src/components/PageHeader.tsx +13 -0
  139. package/studio/src/components/SearchBar.tsx +57 -0
  140. package/studio/src/components/Sidebar.tsx +294 -0
  141. package/studio/src/components/ui/button.tsx +56 -0
  142. package/studio/src/components/ui/checkbox.tsx +26 -0
  143. package/studio/src/components/ui/input.tsx +25 -0
  144. package/studio/src/hooks/useDataTable.ts +131 -0
  145. package/studio/src/index.css +36 -0
  146. package/studio/src/lib/api.ts +186 -0
  147. package/studio/src/lib/utils.ts +13 -0
  148. package/studio/src/main.tsx +17 -0
  149. package/studio/src/pages/ArcheType.tsx +239 -0
  150. package/studio/src/pages/Components.tsx +124 -0
  151. package/studio/src/pages/EntityInspector.tsx +302 -0
  152. package/studio/src/pages/QueryRunner.tsx +246 -0
  153. package/studio/src/pages/Table.tsx +94 -0
  154. package/studio/src/pages/Welcome.tsx +241 -0
  155. package/studio/src/routes.tsx +45 -0
  156. package/studio/src/store/archeTypeSettings.ts +30 -0
  157. package/studio/src/store/studio.ts +65 -0
  158. package/studio/src/utils/columnHelpers.tsx +114 -0
  159. package/studio/studio-instructions.md +81 -0
  160. package/studio/tailwind.config.js +77 -0
  161. package/studio/tsconfig.json +24 -0
  162. package/studio/utils.ts +54 -0
  163. package/studio/vite.config.js +19 -0
  164. package/swagger/generator.ts +1 -1
  165. package/tests/e2e/http.test.ts +126 -0
  166. package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
  167. package/tests/fixtures/components/TestOrder.ts +23 -0
  168. package/tests/fixtures/components/TestProduct.ts +23 -0
  169. package/tests/fixtures/components/TestUser.ts +20 -0
  170. package/tests/fixtures/components/index.ts +6 -0
  171. package/tests/graphql/SchemaGeneration.test.ts +90 -0
  172. package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
  173. package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
  174. package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
  175. package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
  176. package/tests/integration/entity/Entity.persistence.test.ts +333 -0
  177. package/tests/integration/query/Query.exec.test.ts +523 -0
  178. package/tests/pglite-setup.ts +61 -0
  179. package/tests/setup.ts +164 -0
  180. package/tests/stress/BenchmarkRunner.ts +203 -0
  181. package/tests/stress/DataSeeder.ts +190 -0
  182. package/tests/stress/StressTestReporter.ts +229 -0
  183. package/tests/stress/cursor-perf-test.ts +171 -0
  184. package/tests/stress/fixtures/StressTestComponents.ts +58 -0
  185. package/tests/stress/index.ts +7 -0
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
  187. package/tests/unit/BatchLoader.test.ts +82 -0
  188. package/tests/unit/archetype/ArcheType.test.ts +107 -0
  189. package/tests/unit/cache/CacheManager.test.ts +347 -0
  190. package/tests/unit/cache/MemoryCache.test.ts +260 -0
  191. package/tests/unit/cache/RedisCache.test.ts +411 -0
  192. package/tests/unit/entity/Entity.components.test.ts +244 -0
  193. package/tests/unit/entity/Entity.test.ts +345 -0
  194. package/tests/unit/gql/depthLimit.test.ts +203 -0
  195. package/tests/unit/gql/operationMiddleware.test.ts +293 -0
  196. package/tests/unit/health/Health.test.ts +129 -0
  197. package/tests/unit/middleware/AccessLog.test.ts +37 -0
  198. package/tests/unit/middleware/Middleware.test.ts +98 -0
  199. package/tests/unit/middleware/RequestId.test.ts +54 -0
  200. package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
  201. package/tests/unit/query/FilterBuilder.test.ts +111 -0
  202. package/tests/unit/query/Query.test.ts +308 -0
  203. package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
  204. package/tests/unit/schema/schema-integration.test.ts +426 -0
  205. package/tests/unit/schema/schema.test.ts +580 -0
  206. package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
  207. package/tests/unit/upload/RestUpload.test.ts +267 -0
  208. package/tests/unit/validateEnv.test.ts +82 -0
  209. package/tests/utils/entity-tracker.ts +57 -0
  210. package/tests/utils/index.ts +13 -0
  211. package/tests/utils/test-context.ts +149 -0
  212. package/tsconfig.json +5 -1
  213. package/types/archetype.types.ts +6 -0
  214. package/types/hooks.types.ts +1 -1
  215. package/types/query.types.ts +110 -0
  216. package/types/scheduler.types.ts +68 -7
  217. package/types/upload.types.ts +1 -0
  218. package/{core → upload}/FileValidator.ts +10 -1
  219. package/upload/RestUpload.ts +130 -0
  220. package/{core/components → upload}/UploadComponent.ts +11 -11
  221. package/{core → upload}/UploadManager.ts +3 -3
  222. package/upload/index.ts +23 -7
  223. package/utils/UploadHelper.ts +27 -6
  224. package/utils/cronParser.ts +16 -6
  225. package/.github/workflows/deploy-docs.yml +0 -57
  226. package/core/Components.ts +0 -202
  227. package/core/EntityCache.ts +0 -15
  228. package/core/Query.ts +0 -880
  229. package/docs/README.md +0 -149
  230. package/docs/_coverpage.md +0 -36
  231. package/docs/_sidebar.md +0 -23
  232. package/docs/api/core.md +0 -568
  233. package/docs/api/hooks.md +0 -554
  234. package/docs/api/index.md +0 -222
  235. package/docs/api/query.md +0 -678
  236. package/docs/api/service.md +0 -744
  237. package/docs/core-concepts/archetypes.md +0 -512
  238. package/docs/core-concepts/components.md +0 -498
  239. package/docs/core-concepts/entity.md +0 -314
  240. package/docs/core-concepts/hooks.md +0 -683
  241. package/docs/core-concepts/query.md +0 -588
  242. package/docs/core-concepts/services.md +0 -647
  243. package/docs/examples/code-examples.md +0 -425
  244. package/docs/getting-started.md +0 -337
  245. package/docs/index.html +0 -97
  246. package/tests/bench/insert.bench.ts +0 -60
  247. package/tests/bench/relations.bench.ts +0 -270
  248. package/tests/bench/sorting.bench.ts +0 -416
  249. package/tests/component-hooks-simple.test.ts +0 -117
  250. package/tests/component-hooks.test.ts +0 -1461
  251. package/tests/component.test.ts +0 -339
  252. package/tests/errorHandling.test.ts +0 -155
  253. package/tests/hooks.test.ts +0 -667
  254. package/tests/query-sorting.test.ts +0 -101
  255. package/tests/query.test.ts +0 -81
  256. package/tests/relations.test.ts +0 -170
  257. 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
+ }