bunsane 0.3.2 → 0.5.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 (220) hide show
  1. package/CHANGELOG.md +471 -370
  2. package/core/BatchLoader.ts +56 -32
  3. package/core/Entity.ts +93 -1020
  4. package/core/EntityHookManager.ts +52 -754
  5. package/core/Logger.ts +10 -0
  6. package/core/RequestContext.ts +94 -85
  7. package/core/RequestLoaders.ts +98 -5
  8. package/core/SchedulerManager.ts +28 -600
  9. package/core/app/cors.ts +2 -11
  10. package/core/app/preparedStatementWarmup.ts +9 -49
  11. package/core/app/requestRouter.ts +9 -8
  12. package/core/app/restRegistry.ts +8 -0
  13. package/core/archetype/fieldResolvers.ts +85 -40
  14. package/core/archetype/relationLoader.ts +135 -92
  15. package/core/cache/CacheManager.ts +91 -302
  16. package/core/cache/CompressionUtils.ts +34 -3
  17. package/core/cache/MemoryCache.ts +40 -37
  18. package/core/cache/RedisCache.ts +8 -7
  19. package/core/cache/health.ts +30 -0
  20. package/core/cache/invalidation.ts +96 -0
  21. package/core/cache/strategies/writeInvalidate.ts +111 -0
  22. package/core/cache/strategies/writeThrough.ts +233 -0
  23. package/core/components/BaseComponent.ts +25 -10
  24. package/core/components/ComponentRegistry.ts +28 -0
  25. package/core/decorators/IndexedField.ts +1 -1
  26. package/core/entity/cacheStrategies.ts +97 -0
  27. package/core/entity/componentAccess.ts +383 -0
  28. package/core/entity/finders.ts +202 -0
  29. package/core/entity/getCacheManager.ts +10 -0
  30. package/core/entity/pendingOps.ts +72 -0
  31. package/core/entity/saveEntity.ts +375 -0
  32. package/core/health.ts +93 -4
  33. package/core/hooks/dispatcher.ts +439 -0
  34. package/core/hooks/guards.ts +155 -0
  35. package/core/hooks/registry.ts +247 -0
  36. package/core/metadata/definitions/Component.ts +1 -1
  37. package/core/metadata/index.ts +15 -4
  38. package/core/middleware/RateLimit.ts +102 -105
  39. package/core/middleware/RequestId.ts +2 -9
  40. package/core/middleware/SecurityHeaders.ts +2 -11
  41. package/core/middleware/headers.ts +28 -0
  42. package/core/remote/OutboxWorker.ts +213 -183
  43. package/core/remote/RemoteManager.ts +401 -400
  44. package/core/remote/StreamConsumer.ts +535 -535
  45. package/core/remote/types.ts +153 -151
  46. package/core/requestScope.ts +34 -0
  47. package/core/scheduler/cronEvaluator.ts +174 -0
  48. package/core/scheduler/lifecycleHooks.ts +21 -0
  49. package/core/scheduler/lockCoordinator.ts +27 -0
  50. package/core/scheduler/metrics.ts +14 -0
  51. package/core/scheduler/taskRunner.ts +420 -0
  52. package/core/validateEnv.ts +10 -0
  53. package/database/DatabaseHelper.ts +128 -101
  54. package/database/IndexingStrategy.ts +72 -2
  55. package/database/PreparedStatementCache.ts +8 -2
  56. package/database/cancellable.ts +35 -22
  57. package/database/index.ts +29 -3
  58. package/database/instrumentedDb.ts +141 -141
  59. package/database/sqlHelpers.ts +3 -1
  60. package/endpoints/archetypes.ts +2 -8
  61. package/endpoints/tables.ts +6 -1
  62. package/gql/index.ts +1 -1
  63. package/gql/schema/index.ts +15 -4
  64. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  65. package/package.json +22 -1
  66. package/query/CTENode.ts +5 -3
  67. package/query/ComponentInclusionNode.ts +245 -14
  68. package/query/OrNode.ts +8 -19
  69. package/query/Query.ts +208 -79
  70. package/query/QueryContext.ts +6 -0
  71. package/query/QueryDAG.ts +7 -2
  72. package/query/membershipSource.ts +66 -0
  73. package/storage/LocalStorageProvider.ts +8 -3
  74. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  75. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  76. package/studio/{index.html → dist/index.html} +3 -2
  77. package/swagger/generator.ts +11 -1
  78. package/upload/UploadManager.ts +8 -6
  79. package/utils/uuid.ts +40 -10
  80. package/.claude/scheduled_tasks.lock +0 -1
  81. package/.claude/settings.local.json +0 -47
  82. package/.prettierrc +0 -4
  83. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  84. package/.serena/memories/architecture.md +0 -154
  85. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  86. package/.serena/memories/code_style_and_conventions.md +0 -76
  87. package/.serena/memories/project_overview.md +0 -43
  88. package/.serena/memories/schema-dsl-plan.md +0 -107
  89. package/.serena/memories/suggested_commands.md +0 -80
  90. package/.serena/memories/typescript-compilation-status.md +0 -54
  91. package/.serena/project.yml +0 -114
  92. package/BunSane.jpg +0 -0
  93. package/CLAUDE.md +0 -198
  94. package/TODO.md +0 -2
  95. package/bun.lock +0 -302
  96. package/bunfig.toml +0 -10
  97. package/docs/RFC_APP_REFACTOR.md +0 -248
  98. package/docs/RFC_REFACTOR_TARGETS.md +0 -251
  99. package/docs/SCALABILITY_PLAN.md +0 -175
  100. package/studio/bun.lock +0 -482
  101. package/studio/package.json +0 -39
  102. package/studio/postcss.config.js +0 -6
  103. package/studio/src/components/DataTable.tsx +0 -211
  104. package/studio/src/components/Layout.tsx +0 -13
  105. package/studio/src/components/PageContainer.tsx +0 -9
  106. package/studio/src/components/PageHeader.tsx +0 -13
  107. package/studio/src/components/SearchBar.tsx +0 -57
  108. package/studio/src/components/Sidebar.tsx +0 -294
  109. package/studio/src/components/ui/button.tsx +0 -56
  110. package/studio/src/components/ui/checkbox.tsx +0 -26
  111. package/studio/src/components/ui/input.tsx +0 -25
  112. package/studio/src/hooks/useDataTable.ts +0 -131
  113. package/studio/src/index.css +0 -36
  114. package/studio/src/lib/api.ts +0 -186
  115. package/studio/src/lib/utils.ts +0 -13
  116. package/studio/src/main.tsx +0 -17
  117. package/studio/src/pages/ArcheType.tsx +0 -239
  118. package/studio/src/pages/Components.tsx +0 -124
  119. package/studio/src/pages/EntityInspector.tsx +0 -302
  120. package/studio/src/pages/QueryRunner.tsx +0 -246
  121. package/studio/src/pages/Table.tsx +0 -94
  122. package/studio/src/pages/Welcome.tsx +0 -241
  123. package/studio/src/routes.tsx +0 -45
  124. package/studio/src/store/archeTypeSettings.ts +0 -30
  125. package/studio/src/store/studio.ts +0 -65
  126. package/studio/src/utils/columnHelpers.tsx +0 -114
  127. package/studio/studio-instructions.md +0 -81
  128. package/studio/tailwind.config.js +0 -77
  129. package/studio/utils.ts +0 -54
  130. package/studio/vite.config.js +0 -19
  131. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  132. package/tests/benchmark/bunfig.toml +0 -9
  133. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  134. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  135. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  136. package/tests/benchmark/fixtures/index.ts +0 -6
  137. package/tests/benchmark/index.ts +0 -22
  138. package/tests/benchmark/noop-preload.ts +0 -3
  139. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  140. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  141. package/tests/benchmark/runners/index.ts +0 -4
  142. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  143. package/tests/benchmark/scripts/generate-db.ts +0 -344
  144. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  145. package/tests/e2e/http.test.ts +0 -130
  146. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  147. package/tests/fixtures/components/TestOrder.ts +0 -23
  148. package/tests/fixtures/components/TestProduct.ts +0 -23
  149. package/tests/fixtures/components/TestUser.ts +0 -20
  150. package/tests/fixtures/components/index.ts +0 -6
  151. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  152. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  153. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  154. package/tests/helpers/MockRedisClient.ts +0 -113
  155. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  156. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  157. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  158. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  159. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  160. package/tests/integration/loaders/RequestLoaders.abort.test.ts +0 -82
  161. package/tests/integration/query/Query.abort.test.ts +0 -66
  162. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  163. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  164. package/tests/integration/query/Query.exec.test.ts +0 -576
  165. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  166. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  167. package/tests/integration/remote/dlq.test.ts +0 -175
  168. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  169. package/tests/integration/remote/outbox.test.ts +0 -130
  170. package/tests/integration/remote/rpc.test.ts +0 -177
  171. package/tests/pglite-setup.ts +0 -62
  172. package/tests/setup.ts +0 -164
  173. package/tests/stress/BenchmarkRunner.ts +0 -203
  174. package/tests/stress/DataSeeder.ts +0 -190
  175. package/tests/stress/StressTestReporter.ts +0 -229
  176. package/tests/stress/cursor-perf-test.ts +0 -171
  177. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  178. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  179. package/tests/stress/index.ts +0 -7
  180. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  181. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  182. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  183. package/tests/unit/BatchLoader.test.ts +0 -196
  184. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  185. package/tests/unit/cache/CacheManager.test.ts +0 -498
  186. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  187. package/tests/unit/cache/RedisCache.test.ts +0 -411
  188. package/tests/unit/database/cancellable.test.ts +0 -81
  189. package/tests/unit/database/instrumentedDb.test.ts +0 -160
  190. package/tests/unit/entity/Entity.components.test.ts +0 -317
  191. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  192. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  193. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  194. package/tests/unit/entity/Entity.test.ts +0 -345
  195. package/tests/unit/gql/depthLimit.test.ts +0 -203
  196. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  197. package/tests/unit/health/Health.test.ts +0 -129
  198. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  199. package/tests/unit/middleware/Middleware.test.ts +0 -98
  200. package/tests/unit/middleware/RequestId.test.ts +0 -54
  201. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  202. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  203. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  204. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  205. package/tests/unit/query/Query.test.ts +0 -310
  206. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  207. package/tests/unit/remote/RemoteError.test.ts +0 -55
  208. package/tests/unit/remote/decorators.test.ts +0 -195
  209. package/tests/unit/remote/metrics.test.ts +0 -115
  210. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  211. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  212. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  213. package/tests/unit/schema/schema-integration.test.ts +0 -426
  214. package/tests/unit/schema/schema.test.ts +0 -580
  215. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  216. package/tests/unit/upload/RestUpload.test.ts +0 -267
  217. package/tests/unit/validateEnv.test.ts +0 -82
  218. package/tests/utils/entity-tracker.ts +0 -57
  219. package/tests/utils/index.ts +0 -13
  220. 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';