bunsane 0.3.1 → 0.4.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 (224) hide show
  1. package/CHANGELOG.md +445 -318
  2. package/config/cache.config.ts +35 -1
  3. package/core/App.ts +24 -1064
  4. package/core/ArcheType.ts +78 -2110
  5. package/core/BatchLoader.ts +56 -32
  6. package/core/Entity.ts +85 -1043
  7. package/core/EntityHookManager.ts +52 -754
  8. package/core/Logger.ts +10 -0
  9. package/core/RequestContext.ts +64 -6
  10. package/core/RequestLoaders.ts +187 -36
  11. package/core/SchedulerManager.ts +28 -600
  12. package/core/app/bootstrap.ts +133 -0
  13. package/core/app/cors.ts +85 -0
  14. package/core/app/graphqlSetup.ts +56 -0
  15. package/core/app/healthEndpoints.ts +31 -0
  16. package/core/app/metricsCollector.ts +27 -0
  17. package/core/app/preparedStatementWarmup.ts +15 -0
  18. package/core/app/processHandlers.ts +43 -0
  19. package/core/app/requestRouter.ts +310 -0
  20. package/core/app/restRegistry.ts +80 -0
  21. package/core/app/shutdown.ts +97 -0
  22. package/core/app/studioRouter.ts +83 -0
  23. package/core/archetype/customTypes.ts +100 -0
  24. package/core/archetype/decorators.ts +171 -0
  25. package/core/archetype/fieldResolvers.ts +666 -0
  26. package/core/archetype/helpers.ts +29 -0
  27. package/core/archetype/relationLoader.ts +161 -0
  28. package/core/archetype/schemaBuilder.ts +141 -0
  29. package/core/archetype/weaver.ts +218 -0
  30. package/core/archetype/zodSchemaBuilder.ts +527 -0
  31. package/core/cache/CacheManager.ts +173 -267
  32. package/core/cache/CompressionUtils.ts +34 -3
  33. package/core/cache/MemoryCache.ts +40 -37
  34. package/core/cache/RedisCache.ts +4 -4
  35. package/core/cache/health.ts +30 -0
  36. package/core/cache/invalidation.ts +96 -0
  37. package/core/cache/strategies/writeInvalidate.ts +111 -0
  38. package/core/cache/strategies/writeThrough.ts +233 -0
  39. package/core/components/BaseComponent.ts +16 -8
  40. package/core/components/ComponentRegistry.ts +28 -0
  41. package/core/decorators/IndexedField.ts +1 -1
  42. package/core/entity/cacheStrategies.ts +97 -0
  43. package/core/entity/componentAccess.ts +364 -0
  44. package/core/entity/finders.ts +202 -0
  45. package/core/entity/pendingOps.ts +72 -0
  46. package/core/entity/saveEntity.ts +377 -0
  47. package/core/hooks/dispatcher.ts +439 -0
  48. package/core/hooks/guards.ts +155 -0
  49. package/core/hooks/registry.ts +247 -0
  50. package/core/metadata/definitions/Component.ts +1 -1
  51. package/core/metadata/index.ts +15 -4
  52. package/core/middleware/AccessLog.ts +8 -1
  53. package/core/middleware/RateLimit.ts +102 -105
  54. package/core/middleware/RequestId.ts +2 -9
  55. package/core/middleware/SecurityHeaders.ts +2 -11
  56. package/core/middleware/headers.ts +28 -0
  57. package/core/remote/OutboxWorker.ts +213 -183
  58. package/core/remote/RemoteManager.ts +401 -400
  59. package/core/remote/types.ts +153 -151
  60. package/core/requestScope.ts +34 -0
  61. package/core/scheduler/cronEvaluator.ts +174 -0
  62. package/core/scheduler/lifecycleHooks.ts +21 -0
  63. package/core/scheduler/lockCoordinator.ts +27 -0
  64. package/core/scheduler/metrics.ts +14 -0
  65. package/core/scheduler/taskRunner.ts +420 -0
  66. package/database/DatabaseHelper.ts +128 -101
  67. package/database/IndexingStrategy.ts +72 -2
  68. package/database/PreparedStatementCache.ts +20 -5
  69. package/database/cancellable.ts +35 -0
  70. package/database/index.ts +15 -3
  71. package/database/instrumentedDb.ts +141 -0
  72. package/endpoints/archetypes.ts +2 -8
  73. package/endpoints/tables.ts +6 -1
  74. package/gql/index.ts +1 -1
  75. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  76. package/package.json +22 -1
  77. package/query/CTENode.ts +5 -3
  78. package/query/ComponentInclusionNode.ts +240 -13
  79. package/query/OrNode.ts +6 -5
  80. package/query/Query.ts +203 -59
  81. package/query/QueryContext.ts +6 -0
  82. package/query/QueryDAG.ts +7 -2
  83. package/query/membershipSource.ts +66 -0
  84. package/storage/LocalStorageProvider.ts +8 -3
  85. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  86. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  87. package/studio/{index.html → dist/index.html} +3 -2
  88. package/swagger/generator.ts +11 -1
  89. package/upload/UploadManager.ts +8 -6
  90. package/utils/uuid.ts +40 -10
  91. package/.claude/settings.local.json +0 -47
  92. package/.prettierrc +0 -4
  93. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  94. package/.serena/memories/architecture.md +0 -154
  95. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  96. package/.serena/memories/code_style_and_conventions.md +0 -76
  97. package/.serena/memories/project_overview.md +0 -43
  98. package/.serena/memories/schema-dsl-plan.md +0 -107
  99. package/.serena/memories/suggested_commands.md +0 -80
  100. package/.serena/memories/typescript-compilation-status.md +0 -54
  101. package/.serena/project.yml +0 -114
  102. package/BunSane.jpg +0 -0
  103. package/CLAUDE.md +0 -198
  104. package/TODO.md +0 -2
  105. package/bun.lock +0 -302
  106. package/bunfig.toml +0 -10
  107. package/docs/SCALABILITY_PLAN.md +0 -175
  108. package/studio/bun.lock +0 -482
  109. package/studio/package.json +0 -39
  110. package/studio/postcss.config.js +0 -6
  111. package/studio/src/components/DataTable.tsx +0 -211
  112. package/studio/src/components/Layout.tsx +0 -13
  113. package/studio/src/components/PageContainer.tsx +0 -9
  114. package/studio/src/components/PageHeader.tsx +0 -13
  115. package/studio/src/components/SearchBar.tsx +0 -57
  116. package/studio/src/components/Sidebar.tsx +0 -294
  117. package/studio/src/components/ui/button.tsx +0 -56
  118. package/studio/src/components/ui/checkbox.tsx +0 -26
  119. package/studio/src/components/ui/input.tsx +0 -25
  120. package/studio/src/hooks/useDataTable.ts +0 -131
  121. package/studio/src/index.css +0 -36
  122. package/studio/src/lib/api.ts +0 -186
  123. package/studio/src/lib/utils.ts +0 -13
  124. package/studio/src/main.tsx +0 -17
  125. package/studio/src/pages/ArcheType.tsx +0 -239
  126. package/studio/src/pages/Components.tsx +0 -124
  127. package/studio/src/pages/EntityInspector.tsx +0 -302
  128. package/studio/src/pages/QueryRunner.tsx +0 -246
  129. package/studio/src/pages/Table.tsx +0 -94
  130. package/studio/src/pages/Welcome.tsx +0 -241
  131. package/studio/src/routes.tsx +0 -45
  132. package/studio/src/store/archeTypeSettings.ts +0 -30
  133. package/studio/src/store/studio.ts +0 -65
  134. package/studio/src/utils/columnHelpers.tsx +0 -114
  135. package/studio/studio-instructions.md +0 -81
  136. package/studio/tailwind.config.js +0 -77
  137. package/studio/utils.ts +0 -54
  138. package/studio/vite.config.js +0 -19
  139. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  140. package/tests/benchmark/bunfig.toml +0 -9
  141. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  142. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  143. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  144. package/tests/benchmark/fixtures/index.ts +0 -6
  145. package/tests/benchmark/index.ts +0 -22
  146. package/tests/benchmark/noop-preload.ts +0 -3
  147. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  148. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  149. package/tests/benchmark/runners/index.ts +0 -4
  150. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  151. package/tests/benchmark/scripts/generate-db.ts +0 -344
  152. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  153. package/tests/e2e/http.test.ts +0 -130
  154. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  155. package/tests/fixtures/components/TestOrder.ts +0 -23
  156. package/tests/fixtures/components/TestProduct.ts +0 -23
  157. package/tests/fixtures/components/TestUser.ts +0 -20
  158. package/tests/fixtures/components/index.ts +0 -6
  159. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  160. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  161. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  162. package/tests/helpers/MockRedisClient.ts +0 -113
  163. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  164. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  165. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  166. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  167. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  168. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  169. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  170. package/tests/integration/query/Query.exec.test.ts +0 -576
  171. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  172. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  173. package/tests/integration/remote/dlq.test.ts +0 -175
  174. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  175. package/tests/integration/remote/outbox.test.ts +0 -130
  176. package/tests/integration/remote/rpc.test.ts +0 -177
  177. package/tests/pglite-setup.ts +0 -62
  178. package/tests/setup.ts +0 -164
  179. package/tests/stress/BenchmarkRunner.ts +0 -203
  180. package/tests/stress/DataSeeder.ts +0 -190
  181. package/tests/stress/StressTestReporter.ts +0 -229
  182. package/tests/stress/cursor-perf-test.ts +0 -171
  183. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  184. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  185. package/tests/stress/index.ts +0 -7
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  187. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  188. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  189. package/tests/unit/BatchLoader.test.ts +0 -196
  190. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  191. package/tests/unit/cache/CacheManager.test.ts +0 -367
  192. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  193. package/tests/unit/cache/RedisCache.test.ts +0 -411
  194. package/tests/unit/entity/Entity.components.test.ts +0 -317
  195. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  196. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  197. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  198. package/tests/unit/entity/Entity.test.ts +0 -345
  199. package/tests/unit/gql/depthLimit.test.ts +0 -203
  200. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  201. package/tests/unit/health/Health.test.ts +0 -129
  202. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  203. package/tests/unit/middleware/Middleware.test.ts +0 -98
  204. package/tests/unit/middleware/RequestId.test.ts +0 -54
  205. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  206. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  207. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  208. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  209. package/tests/unit/query/Query.test.ts +0 -310
  210. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  211. package/tests/unit/remote/RemoteError.test.ts +0 -55
  212. package/tests/unit/remote/decorators.test.ts +0 -195
  213. package/tests/unit/remote/metrics.test.ts +0 -115
  214. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  215. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  216. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  217. package/tests/unit/schema/schema-integration.test.ts +0 -426
  218. package/tests/unit/schema/schema.test.ts +0 -580
  219. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  220. package/tests/unit/upload/RestUpload.test.ts +0 -267
  221. package/tests/unit/validateEnv.test.ts +0 -82
  222. package/tests/utils/entity-tracker.ts +0 -57
  223. package/tests/utils/index.ts +0 -13
  224. package/tests/utils/test-context.ts +0 -149
@@ -1,344 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * CLI script to generate persistent PGlite benchmark databases.
4
- *
5
- * This script is self-contained and does not depend on the framework's
6
- * database connection - it writes directly to PGlite.
7
- *
8
- * Usage:
9
- * bun tests/benchmark/scripts/generate-db.ts [tier] [--force] [--all]
10
- *
11
- * Examples:
12
- * bun tests/benchmark/scripts/generate-db.ts xs
13
- * bun tests/benchmark/scripts/generate-db.ts md --force
14
- * bun tests/benchmark/scripts/generate-db.ts --all
15
- */
16
- import { PGlite } from '@electric-sql/pglite';
17
- import { existsSync, rmSync, mkdirSync } from 'node:fs';
18
- import { join, dirname } from 'node:path';
19
- import { fileURLToPath } from 'node:url';
20
- import { createHash } from 'node:crypto';
21
-
22
- import {
23
- SeededRandom,
24
- generateUserData,
25
- generateProductData,
26
- generateOrderData,
27
- generateOrderItemData,
28
- generateReviewData
29
- } from '../fixtures/EcommerceDataGenerators';
30
- import { RelationTracker } from '../fixtures/RelationTracker';
31
-
32
- const __dirname = dirname(fileURLToPath(import.meta.url));
33
- const DATABASES_DIR = join(__dirname, '..', 'databases');
34
-
35
- // Database tier configurations
36
- const TIERS = {
37
- xs: { users: 1000, products: 2000, orders: 3000, orderItems: 3000, reviews: 1000 },
38
- sm: { users: 5000, products: 10000, orders: 15000, orderItems: 15000, reviews: 5000 },
39
- md: { users: 10000, products: 20000, orders: 30000, orderItems: 30000, reviews: 10000 },
40
- lg: { users: 50000, products: 100000, orders: 150000, orderItems: 150000, reviews: 50000 },
41
- xl: { users: 100000, products: 200000, orders: 300000, orderItems: 300000, reviews: 100000 }
42
- } as const;
43
-
44
- type Tier = keyof typeof TIERS;
45
-
46
- const DEFAULT_SEED = 42;
47
- const BATCH_SIZE = 1000;
48
-
49
- // Component names and their type IDs (generated deterministically)
50
- const COMPONENT_TYPE_IDS = new Map<string, string>();
51
-
52
- function generateTypeId(name: string): string {
53
- if (COMPONENT_TYPE_IDS.has(name)) {
54
- return COMPONENT_TYPE_IDS.get(name)!;
55
- }
56
- // Generate a SHA256 hash (64 hex chars, matches framework's metadata-storage.ts)
57
- const typeId = createHash('sha256').update(name).digest('hex');
58
- COMPONENT_TYPE_IDS.set(name, typeId);
59
- return typeId;
60
- }
61
-
62
- // Simple UUID v7 implementation (time-ordered)
63
- function uuidv7(): string {
64
- const now = Date.now();
65
- const timeHex = now.toString(16).padStart(12, '0');
66
- const randomBytes = crypto.getRandomValues(new Uint8Array(10));
67
- const randomHex = Array.from(randomBytes).map(b => b.toString(16).padStart(2, '0')).join('');
68
- return `${timeHex.slice(0, 8)}-${timeHex.slice(8, 12)}-7${randomHex.slice(0, 3)}-${(0x80 | (randomBytes[4]! & 0x3f)).toString(16)}${randomHex.slice(5, 7)}-${randomHex.slice(7, 19)}`;
69
- }
70
-
71
- interface GenerationResult {
72
- tier: Tier;
73
- totalEntities: number;
74
- totalTime: number;
75
- recordsPerSecond: number;
76
- path: string;
77
- }
78
-
79
- async function initializeSchema(pg: PGlite): Promise<void> {
80
- await pg.exec(`
81
- CREATE TABLE IF NOT EXISTS entities (
82
- id UUID PRIMARY KEY,
83
- created_at TIMESTAMPTZ DEFAULT NOW(),
84
- updated_at TIMESTAMPTZ DEFAULT NOW(),
85
- deleted_at TIMESTAMPTZ DEFAULT NULL
86
- );
87
-
88
- CREATE TABLE IF NOT EXISTS components (
89
- id UUID PRIMARY KEY,
90
- entity_id UUID NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
91
- type_id VARCHAR(64) NOT NULL,
92
- name VARCHAR(128),
93
- data JSONB NOT NULL DEFAULT '{}',
94
- created_at TIMESTAMPTZ DEFAULT NOW(),
95
- updated_at TIMESTAMPTZ DEFAULT NOW(),
96
- deleted_at TIMESTAMPTZ DEFAULT NULL
97
- );
98
-
99
- CREATE TABLE IF NOT EXISTS entity_components (
100
- entity_id UUID NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
101
- type_id VARCHAR(64) NOT NULL,
102
- component_id UUID NOT NULL REFERENCES components(id) ON DELETE CASCADE,
103
- created_at TIMESTAMPTZ DEFAULT NOW(),
104
- updated_at TIMESTAMPTZ DEFAULT NOW(),
105
- deleted_at TIMESTAMPTZ DEFAULT NULL,
106
- PRIMARY KEY (entity_id, type_id)
107
- );
108
-
109
- CREATE INDEX IF NOT EXISTS idx_components_entity_id ON components(entity_id);
110
- CREATE INDEX IF NOT EXISTS idx_components_type_id ON components(type_id);
111
- CREATE INDEX IF NOT EXISTS idx_components_name ON components(name);
112
- CREATE INDEX IF NOT EXISTS idx_entity_components_type_id ON entity_components(type_id);
113
- CREATE INDEX IF NOT EXISTS idx_entities_deleted_null ON entities(id) WHERE deleted_at IS NULL;
114
- `);
115
- }
116
-
117
- async function seedComponent(
118
- pg: PGlite,
119
- componentName: string,
120
- count: number,
121
- dataGenerator: (index: number) => Record<string, any>,
122
- tracker: RelationTracker,
123
- trackFn?: (entityId: string, data: Record<string, any>) => void,
124
- onProgress?: (current: number) => void
125
- ): Promise<string[]> {
126
- const typeId = generateTypeId(componentName);
127
- const entityIds: string[] = [];
128
-
129
- for (let i = 0; i < count; i += BATCH_SIZE) {
130
- const batchSize = Math.min(BATCH_SIZE, count - i);
131
- const now = new Date().toISOString();
132
-
133
- let entitiesValues = '';
134
- let componentsValues = '';
135
- let entityComponentsValues = '';
136
-
137
- for (let j = 0; j < batchSize; j++) {
138
- const entityId = uuidv7();
139
- const componentId = uuidv7();
140
- const data = dataGenerator(i + j);
141
-
142
- entityIds.push(entityId);
143
-
144
- if (trackFn) {
145
- trackFn(entityId, data);
146
- }
147
-
148
- const sep = j > 0 ? ',' : '';
149
- entitiesValues += `${sep}('${entityId}', '${now}', '${now}')`;
150
- componentsValues += `${sep}('${componentId}', '${entityId}', '${typeId}', '${componentName}', '${JSON.stringify(data).replace(/'/g, "''")}', '${now}', '${now}')`;
151
- entityComponentsValues += `${sep}('${entityId}', '${typeId}', '${componentId}', '${now}', '${now}')`;
152
- }
153
-
154
- await pg.exec(`INSERT INTO entities (id, created_at, updated_at) VALUES ${entitiesValues}`);
155
- await pg.exec(`INSERT INTO components (id, entity_id, type_id, name, data, created_at, updated_at) VALUES ${componentsValues}`);
156
- await pg.exec(`INSERT INTO entity_components (entity_id, type_id, component_id, created_at, updated_at) VALUES ${entityComponentsValues} ON CONFLICT (entity_id, type_id) DO NOTHING`);
157
-
158
- if (onProgress) {
159
- onProgress(i + batchSize);
160
- }
161
- }
162
-
163
- return entityIds;
164
- }
165
-
166
- async function generateDatabase(tier: Tier, force: boolean): Promise<GenerationResult> {
167
- const config = TIERS[tier];
168
- const dbPath = join(DATABASES_DIR, tier);
169
-
170
- if (existsSync(dbPath)) {
171
- if (!force) {
172
- console.log(`Database for tier '${tier}' already exists at ${dbPath}`);
173
- console.log('Use --force to regenerate');
174
- process.exit(0);
175
- }
176
- console.log(`Removing existing database at ${dbPath}...`);
177
- rmSync(dbPath, { recursive: true, force: true });
178
- }
179
-
180
- mkdirSync(dbPath, { recursive: true });
181
-
182
- console.log(`\n=== Generating ${tier.toUpperCase()} tier database ===`);
183
- console.log(`Path: ${dbPath}`);
184
- console.log(`Configuration:`);
185
- console.log(` Users: ${config.users.toLocaleString()}`);
186
- console.log(` Products: ${config.products.toLocaleString()}`);
187
- console.log(` Orders: ${config.orders.toLocaleString()}`);
188
- console.log(` Order Items: ${config.orderItems.toLocaleString()}`);
189
- console.log(` Reviews: ${config.reviews.toLocaleString()}`);
190
-
191
- const totalEntities = config.users + config.products + config.orders + config.orderItems + config.reviews;
192
- console.log(` Total: ${totalEntities.toLocaleString()}`);
193
- console.log('');
194
-
195
- const startTime = performance.now();
196
-
197
- console.log('Initializing PGlite...');
198
- const pg = new PGlite(dbPath, { relaxedDurability: true });
199
- await pg.waitReady;
200
-
201
- console.log('Creating schema...');
202
- await initializeSchema(pg);
203
-
204
- const tracker = new RelationTracker();
205
- const rng = new SeededRandom(DEFAULT_SEED);
206
-
207
- // Seed Users
208
- console.log('\nSeeding Users...');
209
- const userStart = performance.now();
210
- await seedComponent(
211
- pg,
212
- 'BenchUser',
213
- config.users,
214
- (idx) => generateUserData(idx, rng),
215
- tracker,
216
- (entityId) => tracker.addUser(entityId),
217
- (current) => process.stdout.write(`\r Progress: ${current.toLocaleString()}/${config.users.toLocaleString()}`)
218
- );
219
- console.log(`\n Done in ${((performance.now() - userStart) / 1000).toFixed(1)}s`);
220
-
221
- // Seed Products
222
- console.log('\nSeeding Products...');
223
- const productStart = performance.now();
224
- await seedComponent(
225
- pg,
226
- 'BenchProduct',
227
- config.products,
228
- (idx) => generateProductData(idx, rng),
229
- tracker,
230
- (entityId) => tracker.addProduct(entityId),
231
- (current) => process.stdout.write(`\r Progress: ${current.toLocaleString()}/${config.products.toLocaleString()}`)
232
- );
233
- console.log(`\n Done in ${((performance.now() - productStart) / 1000).toFixed(1)}s`);
234
-
235
- // Seed Orders
236
- console.log('\nSeeding Orders...');
237
- const orderStart = performance.now();
238
- await seedComponent(
239
- pg,
240
- 'BenchOrder',
241
- config.orders,
242
- (idx) => generateOrderData(idx, rng, tracker),
243
- tracker,
244
- (entityId, data) => tracker.addOrder(entityId, data.userId),
245
- (current) => process.stdout.write(`\r Progress: ${current.toLocaleString()}/${config.orders.toLocaleString()}`)
246
- );
247
- console.log(`\n Done in ${((performance.now() - orderStart) / 1000).toFixed(1)}s`);
248
-
249
- // Seed Order Items
250
- console.log('\nSeeding Order Items...');
251
- const itemStart = performance.now();
252
- await seedComponent(
253
- pg,
254
- 'BenchOrderItem',
255
- config.orderItems,
256
- (idx) => generateOrderItemData(idx, rng, tracker),
257
- tracker,
258
- undefined,
259
- (current) => process.stdout.write(`\r Progress: ${current.toLocaleString()}/${config.orderItems.toLocaleString()}`)
260
- );
261
- console.log(`\n Done in ${((performance.now() - itemStart) / 1000).toFixed(1)}s`);
262
-
263
- // Seed Reviews
264
- console.log('\nSeeding Reviews...');
265
- const reviewStart = performance.now();
266
- await seedComponent(
267
- pg,
268
- 'BenchReview',
269
- config.reviews,
270
- (idx) => generateReviewData(idx, rng, tracker),
271
- tracker,
272
- undefined,
273
- (current) => process.stdout.write(`\r Progress: ${current.toLocaleString()}/${config.reviews.toLocaleString()}`)
274
- );
275
- console.log(`\n Done in ${((performance.now() - reviewStart) / 1000).toFixed(1)}s`);
276
-
277
- // Run VACUUM ANALYZE
278
- console.log('\nRunning VACUUM ANALYZE...');
279
- await pg.exec('VACUUM ANALYZE entities');
280
- await pg.exec('VACUUM ANALYZE components');
281
- await pg.exec('VACUUM ANALYZE entity_components');
282
-
283
- console.log('Syncing to disk...');
284
- await pg.close();
285
-
286
- const totalTime = (performance.now() - startTime) / 1000;
287
- const recordsPerSecond = Math.round(totalEntities / totalTime);
288
-
289
- console.log('\n=== Generation Complete ===');
290
- console.log(`Total time: ${totalTime.toFixed(1)}s`);
291
- console.log(`Records/second: ${recordsPerSecond.toLocaleString()}`);
292
- console.log(`Database path: ${dbPath}`);
293
-
294
- return {
295
- tier,
296
- totalEntities,
297
- totalTime,
298
- recordsPerSecond,
299
- path: dbPath
300
- };
301
- }
302
-
303
- // Parse CLI arguments
304
- const args = process.argv.slice(2);
305
- const force = args.includes('--force');
306
- const all = args.includes('--all');
307
- const tierArg = args.find(a => !a.startsWith('--'));
308
-
309
- if (!all && !tierArg) {
310
- console.log('Usage: bun tests/benchmark/scripts/generate-db.ts [tier] [--force] [--all]');
311
- console.log('\nTiers: xs, sm, md, lg, xl');
312
- console.log('\nOptions:');
313
- console.log(' --force Regenerate even if database exists');
314
- console.log(' --all Generate all tiers');
315
- console.log('\nExamples:');
316
- console.log(' bun tests/benchmark/scripts/generate-db.ts xs');
317
- console.log(' bun tests/benchmark/scripts/generate-db.ts md --force');
318
- console.log(' bun tests/benchmark/scripts/generate-db.ts --all');
319
- process.exit(1);
320
- }
321
-
322
- if (all) {
323
- console.log('Generating all database tiers...\n');
324
- const results: GenerationResult[] = [];
325
-
326
- for (const tier of Object.keys(TIERS) as Tier[]) {
327
- results.push(await generateDatabase(tier, force));
328
- console.log('');
329
- }
330
-
331
- console.log('\n=== Summary ===');
332
- for (const r of results) {
333
- console.log(`${r.tier.toUpperCase().padEnd(3)} | ${r.totalEntities.toLocaleString().padStart(10)} entities | ${r.totalTime.toFixed(1).padStart(6)}s | ${r.recordsPerSecond.toLocaleString().padStart(8)} rec/s`);
334
- }
335
- } else {
336
- const tier = tierArg as Tier;
337
- if (!TIERS[tier]) {
338
- console.error(`Unknown tier: ${tier}`);
339
- console.error('Valid tiers: xs, sm, md, lg, xl');
340
- process.exit(1);
341
- }
342
-
343
- await generateDatabase(tier, force);
344
- }
@@ -1,97 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Benchmark runner script.
4
- *
5
- * Loads a pre-generated PGlite database and runs benchmarks against it.
6
- * Sets up the correct environment variables before spawning the test process.
7
- *
8
- * Usage:
9
- * bun tests/benchmark/scripts/run-benchmarks.ts [tier]
10
- * bun tests/benchmark/scripts/run-benchmarks.ts xs
11
- * bun tests/benchmark/scripts/run-benchmarks.ts md
12
- */
13
- import { PGlite } from '@electric-sql/pglite';
14
- import { PGLiteSocketServer } from '@electric-sql/pglite-socket';
15
- import { existsSync } from 'node:fs';
16
- import { join, dirname } from 'node:path';
17
- import { fileURLToPath } from 'node:url';
18
- import { spawn } from 'child_process';
19
-
20
- const __dirname = dirname(fileURLToPath(import.meta.url));
21
- const DATABASES_DIR = join(__dirname, '..', 'databases');
22
- const PORT = 54322;
23
-
24
- type Tier = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
25
-
26
- const tier = (process.argv[2] || 'xs') as Tier;
27
- const dbPath = join(DATABASES_DIR, tier);
28
-
29
- if (!existsSync(dbPath)) {
30
- console.error(`Benchmark database for tier '${tier}' not found at ${dbPath}`);
31
- console.error('\nGenerate it first with:');
32
- console.error(` bun tests/benchmark/scripts/generate-db.ts ${tier}`);
33
- process.exit(1);
34
- }
35
-
36
- console.log(`[benchmark] Loading ${tier.toUpperCase()} tier database from ${dbPath}...`);
37
-
38
- const pg = new PGlite(dbPath);
39
- await pg.waitReady;
40
-
41
- // Verify database has data
42
- const countResult = await pg.query<{ count: string }>('SELECT COUNT(*) as count FROM entities');
43
- const entityCount = parseInt(countResult.rows[0]?.count || '0');
44
-
45
- if (entityCount === 0) {
46
- await pg.close();
47
- console.error(`Benchmark database for tier '${tier}' is empty.`);
48
- console.error('Regenerate with:');
49
- console.error(` bun tests/benchmark/scripts/generate-db.ts ${tier} --force`);
50
- process.exit(1);
51
- }
52
-
53
- console.log(`[benchmark] Loaded ${entityCount.toLocaleString()} entities`);
54
-
55
- const server = new PGLiteSocketServer({ db: pg, port: PORT });
56
- await server.start();
57
- console.log(`[benchmark] Socket server running on port ${PORT}`);
58
-
59
- // Spawn the test process with correct env vars set before import
60
- // Use --config to specify benchmark-specific bunfig without the standard preload
61
- const proc = spawn('bun', ['test', '--config', 'tests/benchmark/bunfig.toml', 'tests/benchmark/scenarios/', '--timeout', '300000'], {
62
- env: {
63
- ...process.env,
64
- SKIP_TEST_DB_SETUP: 'true',
65
- USE_PGLITE: 'true',
66
- BENCHMARK_TIER: tier,
67
- // Clear DB_CONNECTION_URL so individual POSTGRES_* vars take precedence
68
- DB_CONNECTION_URL: '',
69
- POSTGRES_HOST: 'localhost',
70
- POSTGRES_PORT: String(PORT),
71
- POSTGRES_USER: 'postgres',
72
- POSTGRES_PASSWORD: 'postgres',
73
- POSTGRES_DB: 'postgres',
74
- POSTGRES_MAX_CONNECTIONS: '10',
75
- LOG_LEVEL: 'info',
76
- // Disable direct partition access since PGlite uses a single components table
77
- BUNSANE_USE_DIRECT_PARTITION: 'false',
78
- // Disable LATERAL joins - they don't work correctly with INTERSECT queries
79
- BUNSANE_USE_LATERAL_JOINS: 'false',
80
- },
81
- stdio: 'inherit',
82
- cwd: join(__dirname, '..', '..', '..'),
83
- });
84
-
85
- proc.on('exit', async (code) => {
86
- console.log('[benchmark] Stopping server...');
87
- try { await server.stop(); } catch {}
88
- try { await pg.close(); } catch {}
89
- process.exit(code ?? 1);
90
- });
91
-
92
- proc.on('error', async (err) => {
93
- console.error('[benchmark] Failed to spawn bun test:', err);
94
- try { await server.stop(); } catch {}
95
- try { await pg.close(); } catch {}
96
- process.exit(1);
97
- });
@@ -1,130 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll } from "bun:test";
2
- import App from "../../core/App";
3
-
4
- const PORT = 19876;
5
- const BASE = `http://localhost:${PORT}`;
6
-
7
- let app: App;
8
-
9
- beforeAll(async () => {
10
- app = new App("E2E Test App", "0.0.1");
11
- // Start without init() — skips DB/component lifecycle
12
- process.env.APP_PORT = String(PORT);
13
- await app.start();
14
- });
15
-
16
- afterAll(async () => {
17
- await app.shutdown();
18
- });
19
-
20
- describe("E2E HTTP Routes", () => {
21
- it("GET /health returns JSON with expected structure", async () => {
22
- const res = await fetch(`${BASE}/health`);
23
- expect(res.headers.get("Content-Type")).toBe("application/json");
24
- const body = await res.json();
25
- expect(body).toHaveProperty("status");
26
- expect(body).toHaveProperty("timestamp");
27
- expect(body).toHaveProperty("uptime");
28
- expect(body).toHaveProperty("checks");
29
- expect(body.checks).toHaveProperty("database");
30
- expect(body.checks).toHaveProperty("cache");
31
- });
32
-
33
- it("GET /health/ready returns 200 when server is up", async () => {
34
- const res = await fetch(`${BASE}/health/ready`);
35
- const body = await res.json();
36
- expect(body).toHaveProperty("status");
37
- expect(body).toHaveProperty("timestamp");
38
- expect(body).toHaveProperty("uptime");
39
- });
40
-
41
- it("GET /metrics returns JSON with process and cache stats", async () => {
42
- const res = await fetch(`${BASE}/metrics`);
43
- expect(res.status).toBe(200);
44
- expect(res.headers.get("Content-Type")).toBe("application/json");
45
- const body = await res.json();
46
- expect(body).toHaveProperty("timestamp");
47
- expect(body).toHaveProperty("uptime");
48
- expect(body).toHaveProperty("process");
49
- expect(body.process).toHaveProperty("rss");
50
- expect(body.process).toHaveProperty("heapUsed");
51
- expect(body).toHaveProperty("scheduler");
52
- expect(body).toHaveProperty("preparedStatements");
53
- });
54
-
55
- it("GET /openapi.json returns valid JSON", async () => {
56
- const res = await fetch(`${BASE}/openapi.json`);
57
- expect(res.status).toBe(200);
58
- expect(res.headers.get("Content-Type")).toBe("application/json");
59
- const body = await res.json();
60
- expect(body).toHaveProperty("openapi");
61
- });
62
-
63
- it("GET /docs returns HTML with swagger-ui", async () => {
64
- const res = await fetch(`${BASE}/docs`);
65
- expect(res.status).toBe(200);
66
- expect(res.headers.get("Content-Type")).toBe("text/html");
67
- const html = await res.text();
68
- expect(html).toContain("swagger-ui");
69
- expect(html).toContain("E2E Test App");
70
- });
71
-
72
- it("GET /nonexistent returns 404", async () => {
73
- const res = await fetch(`${BASE}/nonexistent`);
74
- expect(res.status).toBe(404);
75
- });
76
-
77
- it("OPTIONS /health returns 204 when CORS configured", async () => {
78
- app.setCors({ origin: "*" });
79
- // Preflight must carry Origin header per CORS spec; otherwise the
80
- // server emits no Access-Control-Allow-Origin (no `|| '*'` fallback).
81
- const res = await fetch(`${BASE}/health`, {
82
- method: "OPTIONS",
83
- headers: { Origin: "https://client.example" },
84
- });
85
- expect(res.status).toBe(204);
86
- expect(res.headers.get("Access-Control-Allow-Origin")).toBe("*");
87
- });
88
-
89
- it("Security headers: responses include standard security headers when middleware registered", async () => {
90
- // Import and register the security headers middleware
91
- const { securityHeaders } = await import("../../core/middleware/SecurityHeaders");
92
- app.use(securityHeaders());
93
- // Re-compose middleware to include new middleware - access start() sets composedHandler
94
- // For this test, we need to trigger re-composition. Calling start() again would
95
- // bind another server. Instead, test that middleware works by verifying next request.
96
- // Actually, composedHandler is set in start(), adding middleware after start() won't
97
- // take effect. So we just verify the security headers are NOT present (middleware not active).
98
- const res = await fetch(`${BASE}/health`);
99
- // Middleware was added after start(), so it's not in the composed chain yet.
100
- // This verifies the baseline — security header tests belong in unit tests.
101
- expect(res.headers.get("Content-Type")).toBe("application/json");
102
- });
103
-
104
- it("Shutdown completes without error and is idempotent", async () => {
105
- const shutdownApp = new App("Shutdown Test", "0.0.1");
106
- const shutdownPort = 19877;
107
- process.env.APP_PORT = String(shutdownPort);
108
- await shutdownApp.start();
109
-
110
- // Verify server responds before shutdown
111
- const before = await fetch(`http://localhost:${shutdownPort}/openapi.json`);
112
- expect(before.status).toBe(200);
113
-
114
- // Shutdown completes without throwing
115
- await shutdownApp.shutdown();
116
-
117
- // Second shutdown is a no-op (idempotent)
118
- await shutdownApp.shutdown();
119
-
120
- // Restore port for other tests
121
- process.env.APP_PORT = String(PORT);
122
- });
123
-
124
- it("Request timeout returns 408 for long requests", async () => {
125
- // This is hard to test without a slow endpoint. Verify the timeout
126
- // mechanism exists by checking a fast request completes normally.
127
- const res = await fetch(`${BASE}/openapi.json`);
128
- expect(res.status).toBe(200);
129
- });
130
- });
@@ -1,21 +0,0 @@
1
- /**
2
- * Test archetype for user entities
3
- */
4
- import { BaseArcheType, ArcheType, ArcheTypeField } from '../../../core/ArcheType';
5
- import { TestUser } from '../components/TestUser';
6
- import { TestOrder } from '../components/TestOrder';
7
-
8
- @ArcheType({ name: 'TestUserArchetype' })
9
- export class TestUserArchetype extends BaseArcheType {
10
- @ArcheTypeField(TestUser)
11
- user!: TestUser;
12
- }
13
-
14
- @ArcheType({ name: 'TestUserWithOrdersArchetype' })
15
- export class TestUserWithOrdersArchetype extends BaseArcheType {
16
- @ArcheTypeField(TestUser)
17
- user!: TestUser;
18
-
19
- @ArcheTypeField(TestOrder)
20
- order!: TestOrder;
21
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * Test component representing an order
3
- */
4
- import { BaseComponent } from '../../../core/components/BaseComponent';
5
- import { Component, CompData } from '../../../core/components/Decorators';
6
-
7
- @Component
8
- export class TestOrder extends BaseComponent {
9
- @CompData({ indexed: true })
10
- orderNumber!: string;
11
-
12
- @CompData()
13
- total!: number;
14
-
15
- @CompData()
16
- status!: string;
17
-
18
- @CompData()
19
- createdAt!: Date;
20
-
21
- @CompData({ nullable: true })
22
- notes?: string;
23
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * Test component representing a product
3
- */
4
- import { BaseComponent } from '../../../core/components/BaseComponent';
5
- import { Component, CompData } from '../../../core/components/Decorators';
6
-
7
- @Component
8
- export class TestProduct extends BaseComponent {
9
- @CompData({ indexed: true })
10
- sku!: string;
11
-
12
- @CompData()
13
- name!: string;
14
-
15
- @CompData()
16
- price!: number;
17
-
18
- @CompData({ nullable: true })
19
- description?: string;
20
-
21
- @CompData()
22
- inStock!: boolean;
23
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * Test component representing a user
3
- */
4
- import { BaseComponent } from '../../../core/components/BaseComponent';
5
- import { Component, CompData } from '../../../core/components/Decorators';
6
-
7
- @Component
8
- export class TestUser extends BaseComponent {
9
- @CompData({ indexed: true })
10
- name!: string;
11
-
12
- @CompData({ indexed: true })
13
- email!: string;
14
-
15
- @CompData()
16
- age!: number;
17
-
18
- @CompData({ nullable: true })
19
- bio?: string;
20
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Re-export all test components
3
- */
4
- export { TestUser } from './TestUser';
5
- export { TestProduct } from './TestProduct';
6
- export { TestOrder } from './TestOrder';