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,233 +0,0 @@
1
- /**
2
- * Integration tests for Query.explainAnalyze() and debugMode()
3
- * Tests query plan analysis and debug output
4
- */
5
- import { describe, test, expect, beforeAll, beforeEach } from 'bun:test';
6
- import { Query, FilterOp } from '../../../query/Query';
7
- import { TestUser, TestProduct } from '../../fixtures/components';
8
- import { createTestContext, ensureComponentsRegistered } from '../../utils';
9
-
10
- describe('Query EXPLAIN ANALYZE', () => {
11
- const ctx = createTestContext();
12
-
13
- beforeAll(async () => {
14
- await ensureComponentsRegistered(TestUser, TestProduct);
15
- });
16
-
17
- beforeEach(async () => {
18
- // Create test data for queries
19
- const entity = ctx.tracker.create();
20
- entity.add(TestUser, { name: 'ExplainTest', email: 'explain@example.com', age: 30 });
21
- await entity.save();
22
- });
23
-
24
- describe('explainAnalyze()', () => {
25
- test('returns query execution plan', async () => {
26
- const plan = await new Query()
27
- .with(TestUser)
28
- .explainAnalyze();
29
-
30
- expect(plan).toBeDefined();
31
- expect(typeof plan).toBe('string');
32
- expect(plan.length).toBeGreaterThan(0);
33
- });
34
-
35
- test('plan contains execution timing', async () => {
36
- const plan = await new Query()
37
- .with(TestUser)
38
- .explainAnalyze();
39
-
40
- // EXPLAIN ANALYZE includes actual timing
41
- expect(plan).toMatch(/actual time=/i);
42
- });
43
-
44
- test('plan contains buffer statistics by default', async () => {
45
- const plan = await new Query()
46
- .with(TestUser)
47
- .explainAnalyze(true);
48
-
49
- // BUFFERS option includes shared/local buffer info
50
- expect(plan).toMatch(/Buffers:|shared hit|shared read/i);
51
- });
52
-
53
- test('plan without buffers when buffers=false', async () => {
54
- const plan = await new Query()
55
- .with(TestUser)
56
- .explainAnalyze(false);
57
-
58
- expect(plan).toBeDefined();
59
- // Should still have timing info
60
- expect(plan).toMatch(/actual time=/i);
61
- });
62
-
63
- test('explains filtered query', async () => {
64
- const plan = await new Query()
65
- .with(TestUser, {
66
- filters: [Query.filter('name', FilterOp.EQ, 'ExplainTest')]
67
- })
68
- .explainAnalyze();
69
-
70
- expect(plan).toBeDefined();
71
- // Filter should show in the plan
72
- expect(plan.length).toBeGreaterThan(0);
73
- });
74
-
75
- test('explains query with multiple components', async () => {
76
- // Create entity with multiple components
77
- const entity = ctx.tracker.create();
78
- entity.add(TestUser, { name: 'MultiComp', email: 'multi@example.com', age: 25 });
79
- entity.add(TestProduct, { sku: 'EXPLAIN-SKU', name: 'Explain Product', price: 100, inStock: true });
80
- await entity.save();
81
-
82
- const plan = await new Query()
83
- .with(TestUser)
84
- .with(TestProduct)
85
- .explainAnalyze();
86
-
87
- expect(plan).toBeDefined();
88
- expect(plan.length).toBeGreaterThan(0);
89
- });
90
-
91
- test('explains query with sorting', async () => {
92
- const plan = await new Query()
93
- .with(TestUser)
94
- .sortBy(TestUser, 'age', 'DESC')
95
- .explainAnalyze();
96
-
97
- expect(plan).toBeDefined();
98
- // Sort should show in plan
99
- expect(plan).toMatch(/Sort|sort/i);
100
- });
101
-
102
- test('explains query with limit', async () => {
103
- const plan = await new Query()
104
- .with(TestUser)
105
- .take(10)
106
- .explainAnalyze();
107
-
108
- expect(plan).toBeDefined();
109
- // Limit should show in plan
110
- expect(plan).toMatch(/Limit|limit/i);
111
- });
112
-
113
- test('explains count query equivalent', async () => {
114
- // explainAnalyze on a regular query shows the underlying query plan
115
- // For count analysis, one would wrap the query differently
116
- const plan = await new Query()
117
- .with(TestUser)
118
- .explainAnalyze();
119
-
120
- expect(plan).toBeDefined();
121
- });
122
- });
123
-
124
- describe('debugMode()', () => {
125
- test('debugMode does not throw', async () => {
126
- const results = await new Query()
127
- .with(TestUser)
128
- .debugMode(true)
129
- .exec();
130
-
131
- expect(Array.isArray(results)).toBe(true);
132
- });
133
-
134
- test('debugMode can be disabled', async () => {
135
- const results = await new Query()
136
- .with(TestUser)
137
- .debugMode(true)
138
- .debugMode(false)
139
- .exec();
140
-
141
- expect(Array.isArray(results)).toBe(true);
142
- });
143
-
144
- test('debugMode works with count()', async () => {
145
- const count = await new Query()
146
- .with(TestUser)
147
- .debugMode(true)
148
- .count();
149
-
150
- expect(typeof count).toBe('number');
151
- });
152
-
153
- test('debugMode works with explainAnalyze()', async () => {
154
- const plan = await new Query()
155
- .with(TestUser)
156
- .debugMode(true)
157
- .explainAnalyze();
158
-
159
- expect(plan).toBeDefined();
160
- expect(typeof plan).toBe('string');
161
- });
162
-
163
- test('debugMode works with sum()', async () => {
164
- const entity = ctx.tracker.create();
165
- entity.add(TestProduct, { sku: 'DEBUG-SUM', name: 'Debug Sum', price: 50, inStock: true });
166
- await entity.save();
167
-
168
- const sum = await new Query()
169
- .with(TestProduct)
170
- .debugMode(true)
171
- .sum(TestProduct, 'price');
172
-
173
- expect(typeof sum).toBe('number');
174
- });
175
-
176
- test('debugMode works with average()', async () => {
177
- const entity = ctx.tracker.create();
178
- entity.add(TestProduct, { sku: 'DEBUG-AVG', name: 'Debug Avg', price: 75, inStock: true });
179
- await entity.save();
180
-
181
- const avg = await new Query()
182
- .with(TestProduct)
183
- .debugMode(true)
184
- .average(TestProduct, 'price');
185
-
186
- expect(typeof avg).toBe('number');
187
- });
188
- });
189
-
190
- describe('query plan analysis patterns', () => {
191
- test('index scan shows when filtering on indexed field', async () => {
192
- // Create several entities to make index usage more likely
193
- for (let i = 0; i < 10; i++) {
194
- const entity = ctx.tracker.create();
195
- entity.add(TestUser, {
196
- name: `IndexTest${i}`,
197
- email: `index${i}@example.com`,
198
- age: 20 + i
199
- });
200
- await entity.save();
201
- }
202
-
203
- const plan = await new Query()
204
- .with(TestUser, {
205
- filters: [Query.filter('email', FilterOp.EQ, 'index5@example.com')]
206
- })
207
- .explainAnalyze();
208
-
209
- expect(plan).toBeDefined();
210
- // Plan should show some form of scan
211
- expect(plan).toMatch(/Scan|scan/i);
212
- });
213
-
214
- test('plan shows rows estimation', async () => {
215
- const plan = await new Query()
216
- .with(TestUser)
217
- .explainAnalyze();
218
-
219
- // EXPLAIN ANALYZE shows estimated vs actual rows
220
- expect(plan).toMatch(/rows=/i);
221
- });
222
-
223
- test('plan shows execution time', async () => {
224
- const plan = await new Query()
225
- .with(TestUser)
226
- .take(5)
227
- .explainAnalyze();
228
-
229
- // Planning and execution time at the end
230
- expect(plan).toMatch(/Planning Time:|Execution Time:/i);
231
- });
232
- });
233
- });
@@ -1,214 +0,0 @@
1
- /**
2
- * Integration tests for JSONB Array Query Operators
3
- * Tests CONTAINS (@>), CONTAINED_BY (<@), HAS_ANY (?|), HAS_ALL (?&)
4
- */
5
- import { describe, test, expect, beforeAll, beforeEach } from 'bun:test';
6
- import { Query, FilterOp } from '../../../query/Query';
7
- import { BaseComponent } from '../../../core/components/BaseComponent';
8
- import { Component, CompData } from '../../../core/components/Decorators';
9
- import { createTestContext, ensureComponentsRegistered } from '../../utils';
10
-
11
- @Component
12
- class TaggedItem extends BaseComponent {
13
- @CompData({ indexed: true, arrayOf: String })
14
- tags: string[] = [];
15
-
16
- @CompData({ indexed: true })
17
- name: string = '';
18
- }
19
-
20
- @Component
21
- class CategoryItem extends BaseComponent {
22
- @CompData({ indexed: true })
23
- title: string = '';
24
- }
25
-
26
- const isPGlite = process.env.USE_PGLITE === 'true';
27
-
28
- describe('JSONB Array Query Operators', () => {
29
- const ctx = createTestContext();
30
-
31
- beforeAll(async () => {
32
- await ensureComponentsRegistered(TaggedItem, CategoryItem);
33
- });
34
-
35
- beforeEach(async () => {
36
- // entity1: tags = ["red", "blue"]
37
- const e1 = ctx.tracker.create();
38
- e1.add(TaggedItem, { tags: ['red', 'blue'], name: 'item1' });
39
- await e1.save();
40
-
41
- // entity2: tags = ["blue", "green"]
42
- const e2 = ctx.tracker.create();
43
- e2.add(TaggedItem, { tags: ['blue', 'green'], name: 'item2' });
44
- await e2.save();
45
-
46
- // entity3: tags = ["red", "green", "blue"]
47
- const e3 = ctx.tracker.create();
48
- e3.add(TaggedItem, { tags: ['red', 'green', 'blue'], name: 'item3' });
49
- await e3.save();
50
-
51
- // entity4: tags = ["yellow"]
52
- const e4 = ctx.tracker.create();
53
- e4.add(TaggedItem, { tags: ['yellow'], name: 'item4' });
54
- await e4.save();
55
- });
56
-
57
- describe('CONTAINS (@>)', () => {
58
- test('finds entities where array contains a single value', async () => {
59
- const results = await new Query()
60
- .with(TaggedItem, {
61
- filters: [Query.filter('tags', FilterOp.CONTAINS, 'red')]
62
- })
63
- .exec();
64
-
65
- // item1 (red,blue) and item3 (red,green,blue)
66
- expect(results.length).toBe(2);
67
- });
68
-
69
- test('finds entities where array contains multiple values (AND)', async () => {
70
- const results = await new Query()
71
- .with(TaggedItem, {
72
- filters: [Query.filter('tags', FilterOp.CONTAINS, ['red', 'blue'])]
73
- })
74
- .exec();
75
-
76
- // item1 (red,blue) and item3 (red,green,blue) both have red AND blue
77
- expect(results.length).toBe(2);
78
- });
79
-
80
- test('returns empty when no match', async () => {
81
- const results = await new Query()
82
- .with(TaggedItem, {
83
- filters: [Query.filter('tags', FilterOp.CONTAINS, 'purple')]
84
- })
85
- .exec();
86
-
87
- expect(results.length).toBe(0);
88
- });
89
-
90
- test('single element array matches', async () => {
91
- const results = await new Query()
92
- .with(TaggedItem, {
93
- filters: [Query.filter('tags', FilterOp.CONTAINS, 'yellow')]
94
- })
95
- .exec();
96
-
97
- expect(results.length).toBe(1);
98
- });
99
-
100
- test('combined with other filters', async () => {
101
- const results = await new Query()
102
- .with(TaggedItem, {
103
- filters: [
104
- Query.filter('tags', FilterOp.CONTAINS, 'red'),
105
- Query.filter('name', FilterOp.EQ, 'item1'),
106
- ]
107
- })
108
- .exec();
109
-
110
- expect(results.length).toBe(1);
111
- });
112
- });
113
-
114
- describe('CONTAINED_BY (<@)', () => {
115
- test('finds entities whose array is a subset of given values', async () => {
116
- const results = await new Query()
117
- .with(TaggedItem, {
118
- filters: [Query.filter('tags', FilterOp.CONTAINED_BY, ['red', 'blue'])]
119
- })
120
- .exec();
121
-
122
- // Only item1 (red,blue) is a subset of [red,blue]
123
- expect(results.length).toBe(1);
124
- });
125
-
126
- test('superset input matches all subsets', async () => {
127
- const results = await new Query()
128
- .with(TaggedItem, {
129
- filters: [Query.filter('tags', FilterOp.CONTAINED_BY, ['red', 'blue', 'green', 'yellow'])]
130
- })
131
- .exec();
132
-
133
- // All 4 entities are subsets
134
- expect(results.length).toBe(4);
135
- });
136
- });
137
-
138
- describe.skipIf(isPGlite)('HAS_ANY (?|)', () => {
139
- test('finds entities with any of the given values', async () => {
140
- const results = await new Query()
141
- .with(TaggedItem, {
142
- filters: [Query.filter('tags', FilterOp.HAS_ANY, ['red', 'yellow'])]
143
- })
144
- .exec();
145
-
146
- // item1 (red), item3 (red), item4 (yellow)
147
- expect(results.length).toBe(3);
148
- });
149
-
150
- test('single value works', async () => {
151
- const results = await new Query()
152
- .with(TaggedItem, {
153
- filters: [Query.filter('tags', FilterOp.HAS_ANY, ['yellow'])]
154
- })
155
- .exec();
156
-
157
- expect(results.length).toBe(1);
158
- });
159
- });
160
-
161
- describe.skipIf(isPGlite)('HAS_ALL (?&)', () => {
162
- test('finds entities with all of the given values', async () => {
163
- const results = await new Query()
164
- .with(TaggedItem, {
165
- filters: [Query.filter('tags', FilterOp.HAS_ALL, ['red', 'green'])]
166
- })
167
- .exec();
168
-
169
- // Only item3 (red,green,blue) has both red AND green
170
- expect(results.length).toBe(1);
171
- });
172
-
173
- test('single value matches all entities containing it', async () => {
174
- const results = await new Query()
175
- .with(TaggedItem, {
176
- filters: [Query.filter('tags', FilterOp.HAS_ALL, ['blue'])]
177
- })
178
- .exec();
179
-
180
- // item1, item2, item3 all have blue
181
- expect(results.length).toBe(3);
182
- });
183
- });
184
-
185
- describe('multi-component INTERSECT compatibility', () => {
186
- test('CONTAINS works with multiple .with() components', async () => {
187
- // Add a second component to one entity
188
- const e = ctx.tracker.create();
189
- e.add(TaggedItem, { tags: ['special'], name: 'multi' });
190
- e.add(CategoryItem, { title: 'test-category' });
191
- await e.save();
192
-
193
- const results = await new Query()
194
- .with(TaggedItem, {
195
- filters: [Query.filter('tags', FilterOp.CONTAINS, 'special')]
196
- })
197
- .with(CategoryItem)
198
- .exec();
199
-
200
- expect(results.length).toBe(1);
201
- });
202
- });
203
-
204
- describe('validation', () => {
205
- test('rejects null value via validator', () => {
206
- expect(() => {
207
- new Query()
208
- .with(TaggedItem, {
209
- filters: [Query.filter('tags', FilterOp.CONTAINS, null)]
210
- });
211
- }).not.toThrow(); // Filter creation succeeds, validation happens at exec time
212
- });
213
- });
214
- });
@@ -1,175 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
- import { RemoteManager } from "../../../core/remote/RemoteManager";
3
- import { MockRedisStreamServer } from "../../helpers/MockRedisStreamServer";
4
- import { createMockRedisFactory } from "../../helpers/MockRedisClient";
5
-
6
- const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
7
-
8
- describe("Dead Letter Queue", () => {
9
- let server: MockRedisStreamServer;
10
- let producer: RemoteManager;
11
-
12
- beforeEach(async () => {
13
- server = new MockRedisStreamServer();
14
- producer = new RemoteManager({
15
- appName: "prod",
16
- redisFactory: createMockRedisFactory(server),
17
- blockMs: 30,
18
- autoClaimIdleMs: 0,
19
- shutdownDrainMs: 100,
20
- });
21
- await producer.start();
22
- });
23
-
24
- afterEach(async () => {
25
- await producer.shutdown();
26
- });
27
-
28
- test("poison message routed to DLQ after second delivery", async () => {
29
- // Consumer 1: handler fails → message stays in PEL with deliveryCount=1
30
- const c1 = new RemoteManager({
31
- appName: "cons",
32
- redisFactory: createMockRedisFactory(server),
33
- blockMs: 30,
34
- autoClaimIdleMs: 0,
35
- dlqMaxDeliveries: 2,
36
- shutdownDrainMs: 100,
37
- });
38
- await c1.start();
39
- c1.on(
40
- "poison",
41
- async () => {
42
- throw new Error("always fails");
43
- },
44
- "h1"
45
- );
46
- await producer.emit("cons", "poison", { bad: true });
47
- await wait(200);
48
- expect(server.getPelSize("remote:cons", "cons")).toBe(1);
49
- await c1.shutdown();
50
-
51
- // Consumer 2: autoClaimIdleMs > 0 triggers XAUTOCLAIM on startup →
52
- // claims the orphan, deliveryCount becomes 2 → DLQ check fires.
53
- const c2 = new RemoteManager({
54
- appName: "cons",
55
- redisFactory: createMockRedisFactory(server),
56
- blockMs: 30,
57
- autoClaimIdleMs: 1,
58
- dlqMaxDeliveries: 2,
59
- shutdownDrainMs: 100,
60
- enableLogging: true,
61
- });
62
- await c2.start();
63
- c2.on(
64
- "poison",
65
- async () => {
66
- throw new Error("still fails");
67
- },
68
- "h1"
69
- );
70
- await wait(300);
71
-
72
- expect(server.getStreamLength("remote:cons:dlq")).toBe(1);
73
- const snap = c2.getMetrics();
74
- expect(snap.events.dlq).toBe(1);
75
-
76
- await c2.shutdown();
77
- });
78
-
79
- test("dlqMaxDeliveries=0 disables DLQ routing", async () => {
80
- const c1 = new RemoteManager({
81
- appName: "cons2",
82
- redisFactory: createMockRedisFactory(server),
83
- blockMs: 30,
84
- autoClaimIdleMs: 0,
85
- dlqMaxDeliveries: 0,
86
- shutdownDrainMs: 100,
87
- });
88
- await c1.start();
89
- c1.on(
90
- "fail",
91
- async () => {
92
- throw new Error("x");
93
- },
94
- "h1"
95
- );
96
- await producer.emit("cons2", "fail", {});
97
- await wait(200);
98
- await c1.shutdown();
99
-
100
- const c2 = new RemoteManager({
101
- appName: "cons2",
102
- redisFactory: createMockRedisFactory(server),
103
- blockMs: 30,
104
- autoClaimIdleMs: 1,
105
- dlqMaxDeliveries: 0,
106
- shutdownDrainMs: 100,
107
- });
108
- await c2.start();
109
- c2.on(
110
- "fail",
111
- async () => {
112
- throw new Error("x");
113
- },
114
- "h1"
115
- );
116
- await wait(300);
117
-
118
- expect(server.getStreamLength("remote:cons2:dlq")).toBe(0);
119
-
120
- await c2.shutdown();
121
- });
122
-
123
- test("DLQ entry carries original_id + delivery_count metadata", async () => {
124
- const c1 = new RemoteManager({
125
- appName: "cons3",
126
- redisFactory: createMockRedisFactory(server),
127
- blockMs: 30,
128
- autoClaimIdleMs: 0,
129
- dlqMaxDeliveries: 2,
130
- shutdownDrainMs: 100,
131
- });
132
- await c1.start();
133
- c1.on(
134
- "p",
135
- async () => {
136
- throw new Error("x");
137
- },
138
- "h1"
139
- );
140
- await producer.emit("cons3", "p", {});
141
- await wait(200);
142
- await c1.shutdown();
143
-
144
- const c2 = new RemoteManager({
145
- appName: "cons3",
146
- redisFactory: createMockRedisFactory(server),
147
- blockMs: 30,
148
- autoClaimIdleMs: 1,
149
- dlqMaxDeliveries: 2,
150
- shutdownDrainMs: 100,
151
- });
152
- await c2.start();
153
- c2.on(
154
- "p",
155
- async () => {
156
- throw new Error("x");
157
- },
158
- "h1"
159
- );
160
- await wait(300);
161
-
162
- const dlqEntries = server.xrange("remote:cons3:dlq", "-", "+");
163
- expect(dlqEntries.length).toBe(1);
164
- const [, fields] = dlqEntries[0]!;
165
- // fields = [k1, v1, k2, v2, ...]
166
- const flat = fields as string[];
167
- const idx = (k: string) => flat.indexOf(k);
168
- expect(idx("original_id")).toBeGreaterThanOrEqual(0);
169
- expect(idx("delivery_count")).toBeGreaterThanOrEqual(0);
170
- expect(idx("moved_at")).toBeGreaterThanOrEqual(0);
171
- expect(idx("data")).toBeGreaterThanOrEqual(0);
172
-
173
- await c2.shutdown();
174
- });
175
- });