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