bunsane 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +47 -0
- package/.claude/skills/update-memory.md +74 -0
- package/.prettierrc +4 -0
- package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
- package/.serena/memories/architecture.md +154 -0
- package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
- package/.serena/memories/code_style_and_conventions.md +76 -0
- package/.serena/memories/project_overview.md +43 -0
- package/.serena/memories/schema-dsl-plan.md +107 -0
- package/.serena/memories/suggested_commands.md +80 -0
- package/.serena/memories/typescript-compilation-status.md +54 -0
- package/.serena/project.yml +114 -0
- package/TODO.md +1 -7
- package/bun.lock +150 -4
- package/bunfig.toml +10 -0
- package/config/cache.config.ts +77 -0
- package/config/upload.config.ts +4 -5
- package/core/App.ts +870 -123
- package/core/ArcheType.ts +2268 -377
- package/core/BatchLoader.ts +181 -71
- package/core/Config.ts +153 -0
- package/core/Decorators.ts +4 -1
- package/core/Entity.ts +621 -92
- package/core/EntityHookManager.ts +1 -1
- package/core/EntityInterface.ts +3 -1
- package/core/EntityManager.ts +1 -13
- package/core/ErrorHandler.ts +8 -2
- package/core/Logger.ts +9 -0
- package/core/Middleware.ts +34 -0
- package/core/RequestContext.ts +5 -1
- package/core/RequestLoaders.ts +227 -93
- package/core/SchedulerManager.ts +193 -52
- package/core/cache/CacheAnalytics.ts +399 -0
- package/core/cache/CacheFactory.ts +145 -0
- package/core/cache/CacheManager.ts +520 -0
- package/core/cache/CacheProvider.ts +34 -0
- package/core/cache/CacheWarmer.ts +157 -0
- package/core/cache/CompressionUtils.ts +110 -0
- package/core/cache/MemoryCache.ts +251 -0
- package/core/cache/MultiLevelCache.ts +180 -0
- package/core/cache/NoOpCache.ts +53 -0
- package/core/cache/RedisCache.ts +464 -0
- package/core/cache/TTLStrategy.ts +254 -0
- package/core/cache/index.ts +6 -0
- package/core/components/BaseComponent.ts +120 -0
- package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
- package/core/components/Decorators.ts +88 -0
- package/core/components/Interfaces.ts +7 -0
- package/core/components/index.ts +5 -0
- package/core/decorators/EntityHooks.ts +0 -3
- package/core/decorators/IndexedField.ts +26 -0
- package/core/decorators/ScheduledTask.ts +0 -47
- package/core/events/EntityLifecycleEvents.ts +1 -1
- package/core/health.ts +112 -0
- package/core/metadata/definitions/ArcheType.ts +14 -0
- package/core/metadata/definitions/Component.ts +9 -0
- package/core/metadata/definitions/gqlObject.ts +1 -1
- package/core/metadata/index.ts +42 -1
- package/core/metadata/metadata-storage.ts +28 -2
- package/core/middleware/AccessLog.ts +59 -0
- package/core/middleware/RequestId.ts +38 -0
- package/core/middleware/SecurityHeaders.ts +62 -0
- package/core/middleware/index.ts +3 -0
- package/core/scheduler/DistributedLock.ts +266 -0
- package/core/scheduler/index.ts +15 -0
- package/core/validateEnv.ts +92 -0
- package/database/DatabaseHelper.ts +416 -40
- package/database/IndexingStrategy.ts +342 -0
- package/database/PreparedStatementCache.ts +226 -0
- package/database/index.ts +32 -7
- package/database/sqlHelpers.ts +14 -2
- package/endpoints/archetypes.ts +362 -0
- package/endpoints/components.ts +58 -0
- package/endpoints/entity.ts +80 -0
- package/endpoints/index.ts +27 -0
- package/endpoints/query.ts +93 -0
- package/endpoints/stats.ts +76 -0
- package/endpoints/tables.ts +212 -0
- package/endpoints/types.ts +155 -0
- package/gql/ArchetypeOperations.ts +32 -86
- package/gql/Generator.ts +27 -315
- package/gql/GeneratorV2.ts +37 -0
- package/gql/builders/InputTypeBuilder.ts +99 -0
- package/gql/builders/ResolverBuilder.ts +234 -0
- package/gql/builders/TypeDefBuilder.ts +105 -0
- package/gql/builders/index.ts +3 -0
- package/gql/decorators/Upload.ts +1 -1
- package/gql/depthLimit.ts +85 -0
- package/gql/graph/GraphNode.ts +224 -0
- package/gql/graph/SchemaGraph.ts +278 -0
- package/gql/helpers.ts +8 -2
- package/gql/index.ts +56 -4
- package/gql/middleware.ts +79 -0
- package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
- package/gql/orchestration/index.ts +1 -0
- package/gql/scanner/ServiceScanner.ts +347 -0
- package/gql/schema/index.ts +458 -0
- package/gql/strategies/TypeGenerationStrategy.ts +329 -0
- package/gql/types.ts +1 -0
- package/gql/utils/TypeSignature.ts +220 -0
- package/gql/utils/index.ts +1 -0
- package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
- package/gql/visitors/DeduplicationVisitor.ts +82 -0
- package/gql/visitors/GraphVisitor.ts +78 -0
- package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
- package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
- package/gql/visitors/TypeCollectorVisitor.ts +79 -0
- package/gql/visitors/VisitorComposer.ts +96 -0
- package/gql/visitors/index.ts +7 -0
- package/package.json +59 -37
- package/plugins/index.ts +2 -2
- package/query/CTENode.ts +97 -0
- package/query/ComponentInclusionNode.ts +689 -0
- package/query/FilterBuilder.ts +127 -0
- package/query/FilterBuilderRegistry.ts +202 -0
- package/query/OrNode.ts +517 -0
- package/query/OrQuery.ts +42 -0
- package/query/Query.ts +1022 -0
- package/query/QueryContext.ts +170 -0
- package/query/QueryDAG.ts +122 -0
- package/query/QueryNode.ts +65 -0
- package/query/SourceNode.ts +53 -0
- package/query/builders/FullTextSearchBuilder.ts +236 -0
- package/query/index.ts +21 -0
- package/scheduler/index.ts +40 -8
- package/service/Service.ts +2 -1
- package/service/ServiceRegistry.ts +6 -5
- package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
- package/storage/S3StorageProvider.ts +316 -0
- package/{core/storage → storage}/StorageProvider.ts +7 -3
- package/studio/bun.lock +482 -0
- package/studio/index.html +13 -0
- package/studio/package.json +39 -0
- package/studio/postcss.config.js +6 -0
- package/studio/src/components/DataTable.tsx +211 -0
- package/studio/src/components/Layout.tsx +13 -0
- package/studio/src/components/PageContainer.tsx +9 -0
- package/studio/src/components/PageHeader.tsx +13 -0
- package/studio/src/components/SearchBar.tsx +57 -0
- package/studio/src/components/Sidebar.tsx +294 -0
- package/studio/src/components/ui/button.tsx +56 -0
- package/studio/src/components/ui/checkbox.tsx +26 -0
- package/studio/src/components/ui/input.tsx +25 -0
- package/studio/src/hooks/useDataTable.ts +131 -0
- package/studio/src/index.css +36 -0
- package/studio/src/lib/api.ts +186 -0
- package/studio/src/lib/utils.ts +13 -0
- package/studio/src/main.tsx +17 -0
- package/studio/src/pages/ArcheType.tsx +239 -0
- package/studio/src/pages/Components.tsx +124 -0
- package/studio/src/pages/EntityInspector.tsx +302 -0
- package/studio/src/pages/QueryRunner.tsx +246 -0
- package/studio/src/pages/Table.tsx +94 -0
- package/studio/src/pages/Welcome.tsx +241 -0
- package/studio/src/routes.tsx +45 -0
- package/studio/src/store/archeTypeSettings.ts +30 -0
- package/studio/src/store/studio.ts +65 -0
- package/studio/src/utils/columnHelpers.tsx +114 -0
- package/studio/studio-instructions.md +81 -0
- package/studio/tailwind.config.js +77 -0
- package/studio/tsconfig.json +24 -0
- package/studio/utils.ts +54 -0
- package/studio/vite.config.js +19 -0
- package/swagger/generator.ts +1 -1
- package/tests/e2e/http.test.ts +126 -0
- package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
- package/tests/fixtures/components/TestOrder.ts +23 -0
- package/tests/fixtures/components/TestProduct.ts +23 -0
- package/tests/fixtures/components/TestUser.ts +20 -0
- package/tests/fixtures/components/index.ts +6 -0
- package/tests/graphql/SchemaGeneration.test.ts +90 -0
- package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
- package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
- package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
- package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
- package/tests/integration/entity/Entity.persistence.test.ts +333 -0
- package/tests/integration/query/Query.exec.test.ts +523 -0
- package/tests/pglite-setup.ts +61 -0
- package/tests/setup.ts +164 -0
- package/tests/stress/BenchmarkRunner.ts +203 -0
- package/tests/stress/DataSeeder.ts +190 -0
- package/tests/stress/StressTestReporter.ts +229 -0
- package/tests/stress/cursor-perf-test.ts +171 -0
- package/tests/stress/fixtures/StressTestComponents.ts +58 -0
- package/tests/stress/index.ts +7 -0
- package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
- package/tests/unit/BatchLoader.test.ts +82 -0
- package/tests/unit/archetype/ArcheType.test.ts +107 -0
- package/tests/unit/cache/CacheManager.test.ts +347 -0
- package/tests/unit/cache/MemoryCache.test.ts +260 -0
- package/tests/unit/cache/RedisCache.test.ts +411 -0
- package/tests/unit/entity/Entity.components.test.ts +244 -0
- package/tests/unit/entity/Entity.test.ts +345 -0
- package/tests/unit/gql/depthLimit.test.ts +203 -0
- package/tests/unit/gql/operationMiddleware.test.ts +293 -0
- package/tests/unit/health/Health.test.ts +129 -0
- package/tests/unit/middleware/AccessLog.test.ts +37 -0
- package/tests/unit/middleware/Middleware.test.ts +98 -0
- package/tests/unit/middleware/RequestId.test.ts +54 -0
- package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
- package/tests/unit/query/FilterBuilder.test.ts +111 -0
- package/tests/unit/query/Query.test.ts +308 -0
- package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
- package/tests/unit/schema/schema-integration.test.ts +426 -0
- package/tests/unit/schema/schema.test.ts +580 -0
- package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
- package/tests/unit/upload/RestUpload.test.ts +267 -0
- package/tests/unit/validateEnv.test.ts +82 -0
- package/tests/utils/entity-tracker.ts +57 -0
- package/tests/utils/index.ts +13 -0
- package/tests/utils/test-context.ts +149 -0
- package/tsconfig.json +5 -1
- package/types/archetype.types.ts +6 -0
- package/types/hooks.types.ts +1 -1
- package/types/query.types.ts +110 -0
- package/types/scheduler.types.ts +68 -7
- package/types/upload.types.ts +1 -0
- package/{core → upload}/FileValidator.ts +10 -1
- package/upload/RestUpload.ts +130 -0
- package/{core/components → upload}/UploadComponent.ts +11 -11
- package/{core → upload}/UploadManager.ts +3 -3
- package/upload/index.ts +23 -7
- package/utils/UploadHelper.ts +27 -6
- package/utils/cronParser.ts +16 -6
- package/.github/workflows/deploy-docs.yml +0 -57
- package/core/Components.ts +0 -202
- package/core/EntityCache.ts +0 -15
- package/core/Query.ts +0 -880
- package/docs/README.md +0 -149
- package/docs/_coverpage.md +0 -36
- package/docs/_sidebar.md +0 -23
- package/docs/api/core.md +0 -568
- package/docs/api/hooks.md +0 -554
- package/docs/api/index.md +0 -222
- package/docs/api/query.md +0 -678
- package/docs/api/service.md +0 -744
- package/docs/core-concepts/archetypes.md +0 -512
- package/docs/core-concepts/components.md +0 -498
- package/docs/core-concepts/entity.md +0 -314
- package/docs/core-concepts/hooks.md +0 -683
- package/docs/core-concepts/query.md +0 -588
- package/docs/core-concepts/services.md +0 -647
- package/docs/examples/code-examples.md +0 -425
- package/docs/getting-started.md +0 -337
- package/docs/index.html +0 -97
- package/tests/bench/insert.bench.ts +0 -60
- package/tests/bench/relations.bench.ts +0 -270
- package/tests/bench/sorting.bench.ts +0 -416
- package/tests/component-hooks-simple.test.ts +0 -117
- package/tests/component-hooks.test.ts +0 -1461
- package/tests/component.test.ts +0 -339
- package/tests/errorHandling.test.ts +0 -155
- package/tests/hooks.test.ts +0 -667
- package/tests/query-sorting.test.ts +0 -101
- package/tests/query.test.ts +0 -81
- package/tests/relations.test.ts +0 -170
- package/tests/scheduler.test.ts +0 -724
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stress Tests - Query Performance Benchmarks
|
|
3
|
+
*
|
|
4
|
+
* Tests query performance with configurable data volumes
|
|
5
|
+
* Default: 10,000 records (smoke test)
|
|
6
|
+
* Set STRESS_RECORD_COUNT env var for larger tests
|
|
7
|
+
*/
|
|
8
|
+
import { describe, test, beforeAll, afterAll, expect } from 'bun:test';
|
|
9
|
+
import { DataSeeder } from '../DataSeeder';
|
|
10
|
+
import { BenchmarkRunner } from '../BenchmarkRunner';
|
|
11
|
+
import { StressTestReporter } from '../StressTestReporter';
|
|
12
|
+
import { Query, FilterOp } from '../../../query/Query';
|
|
13
|
+
import { StressUser, StressProfile } from '../fixtures/StressTestComponents';
|
|
14
|
+
import { ensureComponentsRegistered } from '../../utils';
|
|
15
|
+
|
|
16
|
+
// Configurable via environment variable
|
|
17
|
+
const RECORD_COUNT = parseInt(process.env.STRESS_RECORD_COUNT || '10000', 10);
|
|
18
|
+
const BATCH_SIZE = Math.min(5000, Math.floor(RECORD_COUNT / 10) || 1000);
|
|
19
|
+
|
|
20
|
+
describe('Stress Tests - Query Performance', () => {
|
|
21
|
+
const seeder = new DataSeeder();
|
|
22
|
+
const benchmark = new BenchmarkRunner();
|
|
23
|
+
const reporter = new StressTestReporter();
|
|
24
|
+
let entityIds: string[] = [];
|
|
25
|
+
let setupTime = 0;
|
|
26
|
+
|
|
27
|
+
beforeAll(async () => {
|
|
28
|
+
const startSetup = performance.now();
|
|
29
|
+
|
|
30
|
+
// Ensure components are registered
|
|
31
|
+
await ensureComponentsRegistered(StressUser, StressProfile);
|
|
32
|
+
|
|
33
|
+
// Wait for index creation to settle (prevents deadlocks from concurrent index creation)
|
|
34
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
35
|
+
|
|
36
|
+
console.log(`\n Seeding ${RECORD_COUNT.toLocaleString()} records...`);
|
|
37
|
+
|
|
38
|
+
const result = await seeder.seed(
|
|
39
|
+
StressUser,
|
|
40
|
+
(i) => ({
|
|
41
|
+
name: `User ${i}`,
|
|
42
|
+
email: `user${i}@stress.test`,
|
|
43
|
+
age: 18 + (i % 62),
|
|
44
|
+
status: ['active', 'inactive', 'pending', 'banned'][i % 4],
|
|
45
|
+
score: Math.random() * 1000,
|
|
46
|
+
createdAt: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000)
|
|
47
|
+
}),
|
|
48
|
+
{
|
|
49
|
+
totalEntities: RECORD_COUNT,
|
|
50
|
+
batchSize: BATCH_SIZE,
|
|
51
|
+
onProgress: (current, total, elapsed) => {
|
|
52
|
+
if (current % (BATCH_SIZE * 2) === 0 || current === total) {
|
|
53
|
+
const pct = ((current / total) * 100).toFixed(1);
|
|
54
|
+
const rate = ((current / elapsed) * 1000).toFixed(0);
|
|
55
|
+
console.log(` Progress: ${pct}% (${rate} records/sec)`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
entityIds = result.entityIds;
|
|
62
|
+
console.log(` Seeded in ${(result.totalTime / 1000).toFixed(1)}s (${result.recordsPerSecond.toFixed(0)} records/sec)`);
|
|
63
|
+
|
|
64
|
+
// Add profile components to 50% of entities
|
|
65
|
+
if (RECORD_COUNT >= 1000) {
|
|
66
|
+
console.log(' Adding profile components to 50% of entities...');
|
|
67
|
+
await seeder.seedAdditionalComponent(
|
|
68
|
+
entityIds.slice(0, Math.floor(entityIds.length / 2)),
|
|
69
|
+
StressProfile,
|
|
70
|
+
(i) => ({
|
|
71
|
+
bio: `This is bio ${i}`,
|
|
72
|
+
avatarUrl: `https://example.com/avatar/${i}.png`,
|
|
73
|
+
verified: i % 3 === 0
|
|
74
|
+
}),
|
|
75
|
+
BATCH_SIZE
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(' Running VACUUM ANALYZE...');
|
|
80
|
+
await seeder.optimize();
|
|
81
|
+
|
|
82
|
+
setupTime = performance.now() - startSetup;
|
|
83
|
+
console.log(` Setup complete in ${(setupTime / 1000).toFixed(1)}s\n`);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterAll(async () => {
|
|
87
|
+
// Print report
|
|
88
|
+
const recordCount = await seeder.getRecordCount();
|
|
89
|
+
const report = reporter.generateReport(benchmark.getResults(), {
|
|
90
|
+
recordCount,
|
|
91
|
+
environment: `PostgreSQL, Bun ${Bun.version}`,
|
|
92
|
+
duration: setupTime
|
|
93
|
+
});
|
|
94
|
+
console.log('\n' + report);
|
|
95
|
+
|
|
96
|
+
// Cleanup seeded data
|
|
97
|
+
console.log('\n Cleaning up test data...');
|
|
98
|
+
await seeder.cleanup(entityIds, BATCH_SIZE);
|
|
99
|
+
console.log(' Cleanup complete.');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('indexed equality filter (status = active)', async () => {
|
|
103
|
+
const result = await benchmark.runWithOutput(
|
|
104
|
+
'Filter: status = active',
|
|
105
|
+
() => new Query()
|
|
106
|
+
.with(StressUser, { filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }] })
|
|
107
|
+
.take(100)
|
|
108
|
+
.exec(),
|
|
109
|
+
{ targetP95: 50, iterations: 15 }
|
|
110
|
+
);
|
|
111
|
+
expect(result.passed).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('indexed range filter (age 25-35)', async () => {
|
|
115
|
+
const result = await benchmark.runWithOutput(
|
|
116
|
+
'Filter: age 25-35',
|
|
117
|
+
() => new Query()
|
|
118
|
+
.with(StressUser, {
|
|
119
|
+
filters: [
|
|
120
|
+
{ field: 'age', operator: FilterOp.GTE, value: 25 },
|
|
121
|
+
{ field: 'age', operator: FilterOp.LTE, value: 35 }
|
|
122
|
+
]
|
|
123
|
+
})
|
|
124
|
+
.take(100)
|
|
125
|
+
.exec(),
|
|
126
|
+
{ targetP95: 75, iterations: 15 }
|
|
127
|
+
);
|
|
128
|
+
expect(result.passed).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('count query', async () => {
|
|
132
|
+
const result = await benchmark.runWithOutput(
|
|
133
|
+
'COUNT all',
|
|
134
|
+
async () => [await new Query().with(StressUser).count()],
|
|
135
|
+
{ targetP95: 100, iterations: 15 }
|
|
136
|
+
);
|
|
137
|
+
expect(result.passed).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('pagination - shallow offset (1000)', async () => {
|
|
141
|
+
const result = await benchmark.runWithOutput(
|
|
142
|
+
'Offset 1000',
|
|
143
|
+
() => new Query()
|
|
144
|
+
.with(StressUser)
|
|
145
|
+
.sortBy(StressUser, 'name', 'ASC')
|
|
146
|
+
.take(100)
|
|
147
|
+
.offset(1000)
|
|
148
|
+
.exec(),
|
|
149
|
+
{ targetP95: 75, iterations: 15 }
|
|
150
|
+
);
|
|
151
|
+
expect(result.passed).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('pagination - deep offset (50% of records)', async () => {
|
|
155
|
+
const deepOffset = Math.floor(RECORD_COUNT / 2);
|
|
156
|
+
const result = await benchmark.runWithOutput(
|
|
157
|
+
`Offset ${deepOffset.toLocaleString()}`,
|
|
158
|
+
() => new Query()
|
|
159
|
+
.with(StressUser)
|
|
160
|
+
.sortBy(StressUser, 'name', 'ASC')
|
|
161
|
+
.take(100)
|
|
162
|
+
.offset(deepOffset)
|
|
163
|
+
.exec(),
|
|
164
|
+
{ targetP95: 500, iterations: 10 }
|
|
165
|
+
);
|
|
166
|
+
// Deep pagination is expected to be slower, use a more lenient target
|
|
167
|
+
expect(result.timings.p95).toBeLessThan(2000);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('cursor pagination - first page', async () => {
|
|
171
|
+
const result = await benchmark.runWithOutput(
|
|
172
|
+
'Cursor: first page',
|
|
173
|
+
() => new Query()
|
|
174
|
+
.with(StressUser)
|
|
175
|
+
.take(100)
|
|
176
|
+
.exec(),
|
|
177
|
+
{ targetP95: 50, iterations: 15 }
|
|
178
|
+
);
|
|
179
|
+
expect(result.passed).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('cursor pagination - from middle (using cursor)', async () => {
|
|
183
|
+
// First, get an entity ID from the middle of the dataset
|
|
184
|
+
const midpointResults = await new Query()
|
|
185
|
+
.with(StressUser)
|
|
186
|
+
.take(1)
|
|
187
|
+
.offset(Math.floor(RECORD_COUNT / 2))
|
|
188
|
+
.exec();
|
|
189
|
+
|
|
190
|
+
const cursorId = midpointResults[0]?.id;
|
|
191
|
+
expect(cursorId).toBeDefined();
|
|
192
|
+
|
|
193
|
+
const result = await benchmark.runWithOutput(
|
|
194
|
+
'Cursor: from middle (O(1))',
|
|
195
|
+
() => new Query()
|
|
196
|
+
.with(StressUser)
|
|
197
|
+
.cursor(cursorId!)
|
|
198
|
+
.take(100)
|
|
199
|
+
.exec(),
|
|
200
|
+
{ targetP95: 50, iterations: 15 }
|
|
201
|
+
);
|
|
202
|
+
// Cursor pagination should be fast regardless of position
|
|
203
|
+
expect(result.passed).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('cursor pagination - near end (using cursor)', async () => {
|
|
207
|
+
// Get an entity ID from near the end (90%)
|
|
208
|
+
const nearEndResults = await new Query()
|
|
209
|
+
.with(StressUser)
|
|
210
|
+
.take(1)
|
|
211
|
+
.offset(Math.floor(RECORD_COUNT * 0.9))
|
|
212
|
+
.exec();
|
|
213
|
+
|
|
214
|
+
const cursorId = nearEndResults[0]?.id;
|
|
215
|
+
expect(cursorId).toBeDefined();
|
|
216
|
+
|
|
217
|
+
const result = await benchmark.runWithOutput(
|
|
218
|
+
'Cursor: near end (O(1))',
|
|
219
|
+
() => new Query()
|
|
220
|
+
.with(StressUser)
|
|
221
|
+
.cursor(cursorId!)
|
|
222
|
+
.take(100)
|
|
223
|
+
.exec(),
|
|
224
|
+
{ targetP95: 50, iterations: 15 }
|
|
225
|
+
);
|
|
226
|
+
// Cursor pagination should be fast regardless of position
|
|
227
|
+
expect(result.passed).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('multi-component join (User + Profile)', async () => {
|
|
231
|
+
const result = await benchmark.runWithOutput(
|
|
232
|
+
'Join: User + Profile',
|
|
233
|
+
() => new Query()
|
|
234
|
+
.with(StressUser)
|
|
235
|
+
.with(StressProfile)
|
|
236
|
+
.take(100)
|
|
237
|
+
.exec(),
|
|
238
|
+
{ targetP95: 150, iterations: 15 }
|
|
239
|
+
);
|
|
240
|
+
expect(result.passed).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('combined filters and sort', async () => {
|
|
244
|
+
const result = await benchmark.runWithOutput(
|
|
245
|
+
'Filter + Sort + Limit',
|
|
246
|
+
() => new Query()
|
|
247
|
+
.with(StressUser, {
|
|
248
|
+
filters: [
|
|
249
|
+
{ field: 'status', operator: FilterOp.EQ, value: 'active' },
|
|
250
|
+
{ field: 'age', operator: FilterOp.GTE, value: 21 }
|
|
251
|
+
]
|
|
252
|
+
})
|
|
253
|
+
.sortBy(StressUser, 'score', 'DESC')
|
|
254
|
+
.take(50)
|
|
255
|
+
.exec(),
|
|
256
|
+
{ targetP95: 100, iterations: 15 }
|
|
257
|
+
);
|
|
258
|
+
expect(result.passed).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('simple query without filters', async () => {
|
|
262
|
+
const result = await benchmark.runWithOutput(
|
|
263
|
+
'Simple: take 100',
|
|
264
|
+
() => new Query()
|
|
265
|
+
.with(StressUser)
|
|
266
|
+
.take(100)
|
|
267
|
+
.exec(),
|
|
268
|
+
{ targetP95: 50, iterations: 15 }
|
|
269
|
+
);
|
|
270
|
+
expect(result.passed).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('query with populate', async () => {
|
|
274
|
+
const result = await benchmark.runWithOutput(
|
|
275
|
+
'Populated: take 50',
|
|
276
|
+
() => new Query()
|
|
277
|
+
.with(StressUser)
|
|
278
|
+
.populate()
|
|
279
|
+
.take(50)
|
|
280
|
+
.exec(),
|
|
281
|
+
{ targetP95: 100, iterations: 15 }
|
|
282
|
+
);
|
|
283
|
+
expect(result.passed).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for BatchLoader TTL behavior
|
|
3
|
+
*/
|
|
4
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
5
|
+
import { BatchLoader } from '../../core/BatchLoader';
|
|
6
|
+
|
|
7
|
+
describe('BatchLoader', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
BatchLoader.clearCache();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('getCacheStats()', () => {
|
|
13
|
+
test('returns zero stats when cache is empty', () => {
|
|
14
|
+
const stats = BatchLoader.getCacheStats();
|
|
15
|
+
expect(stats.size).toBe(0);
|
|
16
|
+
expect(stats.entries).toBe(0);
|
|
17
|
+
expect(stats.expired).toBe(0);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('clearCache()', () => {
|
|
22
|
+
test('clears all cached entries', () => {
|
|
23
|
+
// Access internal cache via any to set up test data
|
|
24
|
+
const cache = (BatchLoader as any).cache as Map<string, Map<string, any>>;
|
|
25
|
+
const innerMap = new Map();
|
|
26
|
+
innerMap.set('parent1', { ids: ['a', 'b'], expiresAt: Date.now() + 300000 });
|
|
27
|
+
cache.set('type1:value', innerMap);
|
|
28
|
+
|
|
29
|
+
expect(BatchLoader.getCacheStats().entries).toBe(1);
|
|
30
|
+
|
|
31
|
+
BatchLoader.clearCache();
|
|
32
|
+
expect(BatchLoader.getCacheStats().entries).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('TTL expiry', () => {
|
|
37
|
+
test('getCacheStats reports expired entries', () => {
|
|
38
|
+
const cache = (BatchLoader as any).cache as Map<string, Map<string, any>>;
|
|
39
|
+
const innerMap = new Map();
|
|
40
|
+
// One fresh entry
|
|
41
|
+
innerMap.set('parent1', { ids: ['a'], expiresAt: Date.now() + 300000 });
|
|
42
|
+
// One expired entry
|
|
43
|
+
innerMap.set('parent2', { ids: ['b'], expiresAt: Date.now() - 1000 });
|
|
44
|
+
cache.set('type1:value', innerMap);
|
|
45
|
+
|
|
46
|
+
const stats = BatchLoader.getCacheStats();
|
|
47
|
+
expect(stats.entries).toBe(2);
|
|
48
|
+
expect(stats.expired).toBe(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('expired entries are counted correctly for multiple type keys', () => {
|
|
52
|
+
const cache = (BatchLoader as any).cache as Map<string, Map<string, any>>;
|
|
53
|
+
|
|
54
|
+
const map1 = new Map();
|
|
55
|
+
map1.set('p1', { ids: ['a'], expiresAt: Date.now() - 5000 });
|
|
56
|
+
map1.set('p2', { ids: ['b'], expiresAt: Date.now() + 300000 });
|
|
57
|
+
cache.set('type1:field', map1);
|
|
58
|
+
|
|
59
|
+
const map2 = new Map();
|
|
60
|
+
map2.set('p3', { ids: ['c'], expiresAt: Date.now() - 1000 });
|
|
61
|
+
cache.set('type2:field', map2);
|
|
62
|
+
|
|
63
|
+
const stats = BatchLoader.getCacheStats();
|
|
64
|
+
expect(stats.size).toBe(2);
|
|
65
|
+
expect(stats.entries).toBe(3);
|
|
66
|
+
expect(stats.expired).toBe(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('all entries fresh means zero expired', () => {
|
|
70
|
+
const cache = (BatchLoader as any).cache as Map<string, Map<string, any>>;
|
|
71
|
+
const innerMap = new Map();
|
|
72
|
+
innerMap.set('p1', { ids: ['a'], expiresAt: Date.now() + 60000 });
|
|
73
|
+
innerMap.set('p2', { ids: ['b'], expiresAt: Date.now() + 60000 });
|
|
74
|
+
innerMap.set('p3', { ids: ['c'], expiresAt: Date.now() + 60000 });
|
|
75
|
+
cache.set('type:field', innerMap);
|
|
76
|
+
|
|
77
|
+
const stats = BatchLoader.getCacheStats();
|
|
78
|
+
expect(stats.entries).toBe(3);
|
|
79
|
+
expect(stats.expired).toBe(0);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for ArcheType system
|
|
3
|
+
* Tests archetype definition and basic functionality
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, expect, beforeAll } from 'bun:test';
|
|
6
|
+
import { BaseArcheType, ArcheType, ArcheTypeField } from '../../../core/ArcheType';
|
|
7
|
+
import { TestUser, TestProduct, TestOrder } from '../../fixtures/components';
|
|
8
|
+
import { TestUserArchetype, TestUserWithOrdersArchetype } from '../../fixtures/archetypes/TestUserArchetype';
|
|
9
|
+
import { ensureComponentsRegistered } from '../../utils';
|
|
10
|
+
|
|
11
|
+
describe('ArcheType', () => {
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
await ensureComponentsRegistered(TestUser, TestProduct, TestOrder);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('ArcheType definition', () => {
|
|
17
|
+
test('archetype class extends BaseArcheType', () => {
|
|
18
|
+
const archetype = new TestUserArchetype();
|
|
19
|
+
expect(archetype).toBeInstanceOf(BaseArcheType);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('archetype has componentMap', () => {
|
|
23
|
+
const archetype = new TestUserArchetype();
|
|
24
|
+
expect(archetype.componentMap).toBeDefined();
|
|
25
|
+
expect(typeof archetype.componentMap).toBe('object');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('componentMap contains declared fields', () => {
|
|
29
|
+
const archetype = new TestUserArchetype();
|
|
30
|
+
expect(archetype.componentMap.user).toBeDefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('archetype with multiple components', () => {
|
|
34
|
+
const archetype = new TestUserWithOrdersArchetype();
|
|
35
|
+
expect(archetype.componentMap.user).toBeDefined();
|
|
36
|
+
expect(archetype.componentMap.order).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('createEntity()', () => {
|
|
41
|
+
test('creates entity with id', () => {
|
|
42
|
+
const archetype = new TestUserArchetype();
|
|
43
|
+
const entity = archetype.createEntity();
|
|
44
|
+
|
|
45
|
+
expect(entity).toBeDefined();
|
|
46
|
+
expect(entity.id).toBeDefined();
|
|
47
|
+
expect(entity.id.length).toBeGreaterThan(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('entity is dirty after creation', () => {
|
|
51
|
+
const archetype = new TestUserArchetype();
|
|
52
|
+
const entity = archetype.createEntity();
|
|
53
|
+
|
|
54
|
+
expect((entity as any)._dirty).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('entity is not persisted after creation', () => {
|
|
58
|
+
const archetype = new TestUserArchetype();
|
|
59
|
+
const entity = archetype.createEntity();
|
|
60
|
+
|
|
61
|
+
expect(entity._persisted).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('getZodObjectSchema()', () => {
|
|
66
|
+
test('returns zod schema for archetype', () => {
|
|
67
|
+
const archetype = new TestUserArchetype();
|
|
68
|
+
const schema = archetype.getZodObjectSchema();
|
|
69
|
+
|
|
70
|
+
expect(schema).toBeDefined();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('getInputSchema()', () => {
|
|
75
|
+
test('returns input schema for archetype', () => {
|
|
76
|
+
const archetype = new TestUserArchetype();
|
|
77
|
+
const schema = archetype.getInputSchema();
|
|
78
|
+
|
|
79
|
+
expect(schema).toBeDefined();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('getComponentsToLoad()', () => {
|
|
84
|
+
test('returns non-empty components array', () => {
|
|
85
|
+
const archetype = new TestUserArchetype();
|
|
86
|
+
const components = (archetype as any).getComponentsToLoad();
|
|
87
|
+
|
|
88
|
+
expect(Array.isArray(components)).toBe(true);
|
|
89
|
+
expect(components.length).toBeGreaterThan(0);
|
|
90
|
+
expect(components).toContain(TestUser);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('withValidation()', () => {
|
|
95
|
+
test('returns a Zod schema with validations applied', () => {
|
|
96
|
+
const archetype = new TestUserArchetype();
|
|
97
|
+
const schema = archetype.withValidation({
|
|
98
|
+
user: { name: 'Valid', email: 'valid@test.com', age: 25 }
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(schema).toBeDefined();
|
|
102
|
+
// Should return a Zod schema with a shape property
|
|
103
|
+
expect(schema.shape).toBeDefined();
|
|
104
|
+
expect(typeof schema.safeParse).toBe('function');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|