bunsane 0.2.4 → 0.2.7

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 (35) hide show
  1. package/core/ArcheType.ts +67 -34
  2. package/core/BatchLoader.ts +215 -30
  3. package/core/Entity.ts +2 -2
  4. package/core/RequestContext.ts +15 -10
  5. package/core/RequestLoaders.ts +4 -2
  6. package/core/cache/CacheProvider.ts +1 -0
  7. package/core/cache/MemoryCache.ts +10 -1
  8. package/core/cache/RedisCache.ts +16 -2
  9. package/core/validateEnv.ts +8 -0
  10. package/database/DatabaseHelper.ts +113 -1
  11. package/database/index.ts +78 -45
  12. package/docs/SCALABILITY_PLAN.md +175 -0
  13. package/package.json +13 -2
  14. package/query/CTENode.ts +44 -24
  15. package/query/ComponentInclusionNode.ts +181 -91
  16. package/query/Query.ts +9 -9
  17. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +338 -0
  18. package/tests/benchmark/bunfig.toml +9 -0
  19. package/tests/benchmark/fixtures/EcommerceComponents.ts +283 -0
  20. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +301 -0
  21. package/tests/benchmark/fixtures/RelationTracker.ts +159 -0
  22. package/tests/benchmark/fixtures/index.ts +6 -0
  23. package/tests/benchmark/index.ts +22 -0
  24. package/tests/benchmark/noop-preload.ts +3 -0
  25. package/tests/benchmark/runners/BenchmarkLoader.ts +132 -0
  26. package/tests/benchmark/runners/index.ts +4 -0
  27. package/tests/benchmark/scenarios/query-benchmarks.test.ts +465 -0
  28. package/tests/benchmark/scripts/generate-db.ts +344 -0
  29. package/tests/benchmark/scripts/run-benchmarks.ts +97 -0
  30. package/tests/integration/query/Query.complexAnalysis.test.ts +557 -0
  31. package/tests/integration/query/Query.explainAnalyze.test.ts +233 -0
  32. package/tests/stress/fixtures/RealisticComponents.ts +235 -0
  33. package/tests/stress/scenarios/realistic-scenarios.test.ts +1081 -0
  34. package/tests/stress/scenarios/timeout-investigation.test.ts +522 -0
  35. package/tests/unit/BatchLoader.test.ts +139 -25
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Unit tests for BatchLoader TTL behavior
2
+ * Unit tests for BatchLoader TTL behavior and bounded cache
3
3
  */
4
4
  import { describe, test, expect, beforeEach } from 'bun:test';
5
5
  import { BatchLoader } from '../../core/BatchLoader';
@@ -16,15 +16,26 @@ describe('BatchLoader', () => {
16
16
  expect(stats.entries).toBe(0);
17
17
  expect(stats.expired).toBe(0);
18
18
  });
19
+
20
+ test('includes memory estimate', () => {
21
+ const stats = BatchLoader.getCacheStats();
22
+ expect(stats.memoryEstimate).toBeDefined();
23
+ expect(typeof stats.memoryEstimate).toBe('string');
24
+ });
19
25
  });
20
26
 
21
27
  describe('clearCache()', () => {
22
28
  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);
29
+ // Access internal cache to set up test data
30
+ const cache = (BatchLoader as any).cache;
31
+ const now = Date.now();
32
+
33
+ // Use the cache's set method to add entries
34
+ cache.set('type1\x00value', 'parent1', {
35
+ ids: ['a', 'b'],
36
+ expiresAt: now + 300000,
37
+ lastAccessed: now
38
+ });
28
39
 
29
40
  expect(BatchLoader.getCacheStats().entries).toBe(1);
30
41
 
@@ -35,13 +46,21 @@ describe('BatchLoader', () => {
35
46
 
36
47
  describe('TTL expiry', () => {
37
48
  test('getCacheStats reports expired entries', () => {
38
- const cache = (BatchLoader as any).cache as Map<string, Map<string, any>>;
39
- const innerMap = new Map();
49
+ const cache = (BatchLoader as any).cache;
50
+ const now = Date.now();
51
+
40
52
  // One fresh entry
41
- innerMap.set('parent1', { ids: ['a'], expiresAt: Date.now() + 300000 });
53
+ cache.set('type1\x00value', 'parent1', {
54
+ ids: ['a'],
55
+ expiresAt: now + 300000,
56
+ lastAccessed: now
57
+ });
42
58
  // One expired entry
43
- innerMap.set('parent2', { ids: ['b'], expiresAt: Date.now() - 1000 });
44
- cache.set('type1:value', innerMap);
59
+ cache.set('type1\x00value', 'parent2', {
60
+ ids: ['b'],
61
+ expiresAt: now - 1000,
62
+ lastAccessed: now - 1000
63
+ });
45
64
 
46
65
  const stats = BatchLoader.getCacheStats();
47
66
  expect(stats.entries).toBe(2);
@@ -49,16 +68,27 @@ describe('BatchLoader', () => {
49
68
  });
50
69
 
51
70
  test('expired entries are counted correctly for multiple type keys', () => {
52
- const cache = (BatchLoader as any).cache as Map<string, Map<string, any>>;
71
+ const cache = (BatchLoader as any).cache;
72
+ const now = Date.now();
53
73
 
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);
74
+ // Type 1 entries
75
+ cache.set('type1\x00field', 'p1', {
76
+ ids: ['a'],
77
+ expiresAt: now - 5000,
78
+ lastAccessed: now - 5000
79
+ });
80
+ cache.set('type1\x00field', 'p2', {
81
+ ids: ['b'],
82
+ expiresAt: now + 300000,
83
+ lastAccessed: now
84
+ });
58
85
 
59
- const map2 = new Map();
60
- map2.set('p3', { ids: ['c'], expiresAt: Date.now() - 1000 });
61
- cache.set('type2:field', map2);
86
+ // Type 2 entry
87
+ cache.set('type2\x00field', 'p3', {
88
+ ids: ['c'],
89
+ expiresAt: now - 1000,
90
+ lastAccessed: now - 1000
91
+ });
62
92
 
63
93
  const stats = BatchLoader.getCacheStats();
64
94
  expect(stats.size).toBe(2);
@@ -67,16 +97,100 @@ describe('BatchLoader', () => {
67
97
  });
68
98
 
69
99
  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);
100
+ const cache = (BatchLoader as any).cache;
101
+ const now = Date.now();
102
+
103
+ cache.set('type\x00field', 'p1', {
104
+ ids: ['a'],
105
+ expiresAt: now + 60000,
106
+ lastAccessed: now
107
+ });
108
+ cache.set('type\x00field', 'p2', {
109
+ ids: ['b'],
110
+ expiresAt: now + 60000,
111
+ lastAccessed: now
112
+ });
113
+ cache.set('type\x00field', 'p3', {
114
+ ids: ['c'],
115
+ expiresAt: now + 60000,
116
+ lastAccessed: now
117
+ });
76
118
 
77
119
  const stats = BatchLoader.getCacheStats();
78
120
  expect(stats.entries).toBe(3);
79
121
  expect(stats.expired).toBe(0);
80
122
  });
81
123
  });
124
+
125
+ describe('pruneExpiredEntries()', () => {
126
+ test('removes expired entries and returns count', () => {
127
+ const cache = (BatchLoader as any).cache;
128
+ const now = Date.now();
129
+
130
+ // Add mix of fresh and expired entries
131
+ cache.set('type\x00field', 'p1', {
132
+ ids: ['a'],
133
+ expiresAt: now - 5000,
134
+ lastAccessed: now - 5000
135
+ });
136
+ cache.set('type\x00field', 'p2', {
137
+ ids: ['b'],
138
+ expiresAt: now + 300000,
139
+ lastAccessed: now
140
+ });
141
+ cache.set('type\x00field', 'p3', {
142
+ ids: ['c'],
143
+ expiresAt: now - 1000,
144
+ lastAccessed: now - 1000
145
+ });
146
+
147
+ expect(BatchLoader.getCacheStats().entries).toBe(3);
148
+ expect(BatchLoader.getCacheStats().expired).toBe(2);
149
+
150
+ const pruned = BatchLoader.pruneExpiredEntries();
151
+ expect(pruned).toBe(2);
152
+
153
+ const statsAfter = BatchLoader.getCacheStats();
154
+ expect(statsAfter.entries).toBe(1);
155
+ expect(statsAfter.expired).toBe(0);
156
+ });
157
+
158
+ test('returns 0 when no expired entries', () => {
159
+ const cache = (BatchLoader as any).cache;
160
+ const now = Date.now();
161
+
162
+ cache.set('type\x00field', 'p1', {
163
+ ids: ['a'],
164
+ expiresAt: now + 60000,
165
+ lastAccessed: now
166
+ });
167
+
168
+ const pruned = BatchLoader.pruneExpiredEntries();
169
+ expect(pruned).toBe(0);
170
+ });
171
+ });
172
+
173
+ describe('bounded cache behavior', () => {
174
+ test('memory estimate increases with entries', () => {
175
+ const cache = (BatchLoader as any).cache;
176
+ const now = Date.now();
177
+
178
+ const statsBefore = BatchLoader.getCacheStats();
179
+
180
+ // Add several entries
181
+ for (let i = 0; i < 100; i++) {
182
+ cache.set(`type${i}\x00field`, `parent${i}`, {
183
+ ids: [`id${i}`],
184
+ expiresAt: now + 60000,
185
+ lastAccessed: now
186
+ });
187
+ }
188
+
189
+ const statsAfter = BatchLoader.getCacheStats();
190
+ expect(statsAfter.entries).toBe(100);
191
+
192
+ // Memory estimate should reflect the entries
193
+ expect(statsAfter.memoryEstimate).toBeDefined();
194
+ });
195
+ });
82
196
  });