@sparkleideas/plugins 3.0.0-alpha.10

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 (80) hide show
  1. package/README.md +401 -0
  2. package/__tests__/collection-manager.test.ts +332 -0
  3. package/__tests__/dependency-graph.test.ts +434 -0
  4. package/__tests__/enhanced-plugin-registry.test.ts +488 -0
  5. package/__tests__/plugin-registry.test.ts +368 -0
  6. package/__tests__/ruvector-bridge.test.ts +2429 -0
  7. package/__tests__/ruvector-integration.test.ts +1602 -0
  8. package/__tests__/ruvector-migrations.test.ts +1099 -0
  9. package/__tests__/ruvector-quantization.test.ts +846 -0
  10. package/__tests__/ruvector-streaming.test.ts +1088 -0
  11. package/__tests__/sdk.test.ts +325 -0
  12. package/__tests__/security.test.ts +348 -0
  13. package/__tests__/utils/ruvector-test-utils.ts +860 -0
  14. package/examples/plugin-creator/index.ts +636 -0
  15. package/examples/plugin-creator/plugin-creator.test.ts +312 -0
  16. package/examples/ruvector/README.md +288 -0
  17. package/examples/ruvector/attention-patterns.ts +394 -0
  18. package/examples/ruvector/basic-usage.ts +288 -0
  19. package/examples/ruvector/docker-compose.yml +75 -0
  20. package/examples/ruvector/gnn-analysis.ts +501 -0
  21. package/examples/ruvector/hyperbolic-hierarchies.ts +557 -0
  22. package/examples/ruvector/init-db.sql +119 -0
  23. package/examples/ruvector/quantization.ts +680 -0
  24. package/examples/ruvector/self-learning.ts +447 -0
  25. package/examples/ruvector/semantic-search.ts +576 -0
  26. package/examples/ruvector/streaming-large-data.ts +507 -0
  27. package/examples/ruvector/transactions.ts +594 -0
  28. package/examples/ruvector-plugins/hook-pattern-library.ts +486 -0
  29. package/examples/ruvector-plugins/index.ts +79 -0
  30. package/examples/ruvector-plugins/intent-router.ts +354 -0
  31. package/examples/ruvector-plugins/mcp-tool-optimizer.ts +424 -0
  32. package/examples/ruvector-plugins/reasoning-bank.ts +657 -0
  33. package/examples/ruvector-plugins/ruvector-plugins.test.ts +518 -0
  34. package/examples/ruvector-plugins/semantic-code-search.ts +498 -0
  35. package/examples/ruvector-plugins/shared/index.ts +20 -0
  36. package/examples/ruvector-plugins/shared/vector-utils.ts +257 -0
  37. package/examples/ruvector-plugins/sona-learning.ts +445 -0
  38. package/package.json +97 -0
  39. package/src/collections/collection-manager.ts +661 -0
  40. package/src/collections/index.ts +56 -0
  41. package/src/collections/official/index.ts +1040 -0
  42. package/src/core/base-plugin.ts +416 -0
  43. package/src/core/plugin-interface.ts +215 -0
  44. package/src/hooks/index.ts +685 -0
  45. package/src/index.ts +378 -0
  46. package/src/integrations/agentic-flow.ts +743 -0
  47. package/src/integrations/index.ts +88 -0
  48. package/src/integrations/ruvector/ARCHITECTURE.md +1245 -0
  49. package/src/integrations/ruvector/attention-advanced.ts +1040 -0
  50. package/src/integrations/ruvector/attention-executor.ts +782 -0
  51. package/src/integrations/ruvector/attention-mechanisms.ts +757 -0
  52. package/src/integrations/ruvector/attention.ts +1063 -0
  53. package/src/integrations/ruvector/gnn.ts +3050 -0
  54. package/src/integrations/ruvector/hyperbolic.ts +1948 -0
  55. package/src/integrations/ruvector/index.ts +394 -0
  56. package/src/integrations/ruvector/migrations/001_create_extension.sql +135 -0
  57. package/src/integrations/ruvector/migrations/002_create_vector_tables.sql +259 -0
  58. package/src/integrations/ruvector/migrations/003_create_indices.sql +328 -0
  59. package/src/integrations/ruvector/migrations/004_create_functions.sql +598 -0
  60. package/src/integrations/ruvector/migrations/005_create_attention_functions.sql +654 -0
  61. package/src/integrations/ruvector/migrations/006_create_gnn_functions.sql +728 -0
  62. package/src/integrations/ruvector/migrations/007_create_hyperbolic_functions.sql +762 -0
  63. package/src/integrations/ruvector/migrations/index.ts +35 -0
  64. package/src/integrations/ruvector/migrations/migrations.ts +647 -0
  65. package/src/integrations/ruvector/quantization.ts +2036 -0
  66. package/src/integrations/ruvector/ruvector-bridge.ts +2000 -0
  67. package/src/integrations/ruvector/self-learning.ts +2376 -0
  68. package/src/integrations/ruvector/streaming.ts +1737 -0
  69. package/src/integrations/ruvector/types.ts +1945 -0
  70. package/src/providers/index.ts +643 -0
  71. package/src/registry/dependency-graph.ts +568 -0
  72. package/src/registry/enhanced-plugin-registry.ts +994 -0
  73. package/src/registry/plugin-registry.ts +604 -0
  74. package/src/sdk/index.ts +563 -0
  75. package/src/security/index.ts +594 -0
  76. package/src/types/index.ts +446 -0
  77. package/src/workers/index.ts +700 -0
  78. package/tmp.json +0 -0
  79. package/tsconfig.json +25 -0
  80. package/vitest.config.ts +23 -0
@@ -0,0 +1,1088 @@
1
+ /**
2
+ * RuVector Streaming Tests
3
+ *
4
+ * Tests for streaming features including:
5
+ * - Streaming large result sets
6
+ * - Backpressure handling
7
+ * - Stream batch inserts
8
+ * - Cursor-based iteration
9
+ *
10
+ * @module @sparkleideas/plugins/__tests__/ruvector-streaming
11
+ */
12
+
13
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
14
+ import { Readable, Writable, Transform } from 'stream';
15
+ import { pipeline } from 'stream/promises';
16
+ import {
17
+ randomVector,
18
+ normalizedVector,
19
+ randomVectors,
20
+ createTestConfig,
21
+ createMockPgPool,
22
+ measureAsync,
23
+ type MockPgPool,
24
+ } from './utils/ruvector-test-utils.js';
25
+
26
+ // ============================================================================
27
+ // Stream Utilities
28
+ // ============================================================================
29
+
30
+ /**
31
+ * Simulates a database cursor for streaming results
32
+ */
33
+ interface Cursor<T> {
34
+ read(batchSize: number): Promise<T[]>;
35
+ close(): Promise<void>;
36
+ position: number;
37
+ exhausted: boolean;
38
+ }
39
+
40
+ /**
41
+ * Creates a mock cursor over a dataset
42
+ */
43
+ function createCursor<T>(data: T[]): Cursor<T> {
44
+ let position = 0;
45
+ let exhausted = false;
46
+
47
+ return {
48
+ async read(batchSize: number): Promise<T[]> {
49
+ if (exhausted) return [];
50
+
51
+ const batch = data.slice(position, position + batchSize);
52
+ position += batch.length;
53
+
54
+ if (position >= data.length) {
55
+ exhausted = true;
56
+ }
57
+
58
+ // Simulate async database read
59
+ await new Promise((resolve) => setTimeout(resolve, 1));
60
+
61
+ return batch;
62
+ },
63
+
64
+ async close(): Promise<void> {
65
+ exhausted = true;
66
+ },
67
+
68
+ get position() {
69
+ return position;
70
+ },
71
+
72
+ get exhausted() {
73
+ return exhausted;
74
+ },
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Vector search result for streaming
80
+ */
81
+ interface StreamSearchResult {
82
+ id: string;
83
+ score: number;
84
+ distance: number;
85
+ vector?: number[];
86
+ metadata?: Record<string, unknown>;
87
+ }
88
+
89
+ /**
90
+ * Stream of search results from cursor
91
+ */
92
+ function createSearchResultStream(
93
+ cursor: Cursor<StreamSearchResult>,
94
+ batchSize: number = 100
95
+ ): Readable {
96
+ return new Readable({
97
+ objectMode: true,
98
+ async read() {
99
+ try {
100
+ const batch = await cursor.read(batchSize);
101
+
102
+ if (batch.length === 0) {
103
+ this.push(null); // End of stream
104
+ return;
105
+ }
106
+
107
+ for (const item of batch) {
108
+ if (!this.push(item)) {
109
+ // Backpressure - pause reading
110
+ return;
111
+ }
112
+ }
113
+ } catch (error) {
114
+ this.destroy(error as Error);
115
+ }
116
+ },
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Transform stream that filters results by score threshold
122
+ */
123
+ function createScoreFilterTransform(minScore: number): Transform {
124
+ return new Transform({
125
+ objectMode: true,
126
+ transform(chunk: StreamSearchResult, encoding, callback) {
127
+ if (chunk.score >= minScore) {
128
+ this.push(chunk);
129
+ }
130
+ callback();
131
+ },
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Transform stream that enriches results with additional data
137
+ */
138
+ function createEnrichmentTransform(
139
+ enrichFn: (result: StreamSearchResult) => Promise<StreamSearchResult>
140
+ ): Transform {
141
+ return new Transform({
142
+ objectMode: true,
143
+ async transform(chunk: StreamSearchResult, encoding, callback) {
144
+ try {
145
+ const enriched = await enrichFn(chunk);
146
+ this.push(enriched);
147
+ callback();
148
+ } catch (error) {
149
+ callback(error as Error);
150
+ }
151
+ },
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Batch write stream for inserting vectors
157
+ */
158
+ interface BatchWriteStream extends Writable {
159
+ batchCount: number;
160
+ totalWritten: number;
161
+ }
162
+
163
+ function createBatchWriteStream(
164
+ insertFn: (batch: Array<{ id: string; vector: number[]; metadata?: Record<string, unknown> }>) => Promise<void>,
165
+ batchSize: number = 100
166
+ ): BatchWriteStream {
167
+ let batch: Array<{ id: string; vector: number[]; metadata?: Record<string, unknown> }> = [];
168
+ let batchCount = 0;
169
+ let totalWritten = 0;
170
+
171
+ const stream = new Writable({
172
+ objectMode: true,
173
+
174
+ async write(chunk, encoding, callback) {
175
+ batch.push(chunk);
176
+
177
+ if (batch.length >= batchSize) {
178
+ try {
179
+ await insertFn(batch);
180
+ totalWritten += batch.length;
181
+ batchCount++;
182
+ batch = [];
183
+ callback();
184
+ } catch (error) {
185
+ callback(error as Error);
186
+ }
187
+ } else {
188
+ callback();
189
+ }
190
+ },
191
+
192
+ async final(callback) {
193
+ if (batch.length > 0) {
194
+ try {
195
+ await insertFn(batch);
196
+ totalWritten += batch.length;
197
+ batchCount++;
198
+ batch = [];
199
+ callback();
200
+ } catch (error) {
201
+ callback(error as Error);
202
+ }
203
+ } else {
204
+ callback();
205
+ }
206
+ },
207
+ }) as BatchWriteStream;
208
+
209
+ Object.defineProperty(stream, 'batchCount', {
210
+ get: () => batchCount,
211
+ });
212
+
213
+ Object.defineProperty(stream, 'totalWritten', {
214
+ get: () => totalWritten,
215
+ });
216
+
217
+ return stream;
218
+ }
219
+
220
+ /**
221
+ * Vector generator stream
222
+ */
223
+ function createVectorGeneratorStream(
224
+ count: number,
225
+ dimensions: number = 384,
226
+ generateMetadata: boolean = true
227
+ ): Readable {
228
+ let generated = 0;
229
+
230
+ return new Readable({
231
+ objectMode: true,
232
+ read() {
233
+ if (generated >= count) {
234
+ this.push(null);
235
+ return;
236
+ }
237
+
238
+ const vector = {
239
+ id: `gen-${Date.now()}-${generated}`,
240
+ vector: normalizedVector(dimensions),
241
+ ...(generateMetadata && {
242
+ metadata: {
243
+ index: generated,
244
+ timestamp: Date.now(),
245
+ batch: Math.floor(generated / 100),
246
+ },
247
+ }),
248
+ };
249
+
250
+ generated++;
251
+ this.push(vector);
252
+ },
253
+ });
254
+ }
255
+
256
+ /**
257
+ * Simulates slow consumer for backpressure testing
258
+ */
259
+ function createSlowConsumer(delayMs: number): Writable {
260
+ return new Writable({
261
+ objectMode: true,
262
+ async write(chunk, encoding, callback) {
263
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
264
+ callback();
265
+ },
266
+ });
267
+ }
268
+
269
+ // ============================================================================
270
+ // Mock Database Operations
271
+ // ============================================================================
272
+
273
+ interface MockStreamingClient {
274
+ searchStream(query: number[], options: {
275
+ k: number;
276
+ metric: 'cosine' | 'euclidean';
277
+ batchSize?: number;
278
+ includeVector?: boolean;
279
+ }): Readable;
280
+
281
+ insertStream(options: {
282
+ tableName: string;
283
+ batchSize?: number;
284
+ }): BatchWriteStream;
285
+
286
+ createCursor(query: string, params?: unknown[]): Promise<Cursor<Record<string, unknown>>>;
287
+
288
+ data: Map<string, { vector: number[]; metadata?: Record<string, unknown> }>;
289
+ }
290
+
291
+ function createMockStreamingClient(): MockStreamingClient {
292
+ const data = new Map<string, { vector: number[]; metadata?: Record<string, unknown> }>();
293
+
294
+ // Pre-populate with test data
295
+ for (let i = 0; i < 10000; i++) {
296
+ data.set(`vec-${i}`, {
297
+ vector: normalizedVector(384),
298
+ metadata: { index: i, category: i % 10 },
299
+ });
300
+ }
301
+
302
+ return {
303
+ data,
304
+
305
+ searchStream(query, options) {
306
+ // Generate mock search results
307
+ const results: StreamSearchResult[] = [];
308
+
309
+ for (const [id, { vector, metadata }] of data) {
310
+ const dot = query.reduce((sum, v, i) => sum + v * vector[i], 0);
311
+ const score = (dot + 1) / 2; // Normalize to 0-1
312
+
313
+ results.push({
314
+ id,
315
+ score,
316
+ distance: 1 - score,
317
+ ...(options.includeVector && { vector }),
318
+ metadata,
319
+ });
320
+ }
321
+
322
+ // Sort by score descending
323
+ results.sort((a, b) => b.score - a.score);
324
+
325
+ // Limit to k results
326
+ const topK = results.slice(0, options.k);
327
+
328
+ // Create cursor and stream
329
+ const cursor = createCursor(topK);
330
+ return createSearchResultStream(cursor, options.batchSize);
331
+ },
332
+
333
+ insertStream(options) {
334
+ return createBatchWriteStream(async (batch) => {
335
+ for (const item of batch) {
336
+ data.set(item.id, {
337
+ vector: item.vector,
338
+ metadata: item.metadata,
339
+ });
340
+ }
341
+ }, options.batchSize);
342
+ },
343
+
344
+ async createCursor(query, params) {
345
+ // Simple mock cursor that returns all data in pages
346
+ const allData = Array.from(data.entries()).map(([id, { vector, metadata }]) => ({
347
+ id,
348
+ vector,
349
+ metadata,
350
+ }));
351
+
352
+ return createCursor(allData);
353
+ },
354
+ };
355
+ }
356
+
357
+ // ============================================================================
358
+ // Test Suites
359
+ // ============================================================================
360
+
361
+ describe('RuVector Streaming', () => {
362
+ let client: MockStreamingClient;
363
+
364
+ beforeEach(() => {
365
+ client = createMockStreamingClient();
366
+ });
367
+
368
+ // ==========================================================================
369
+ // Streaming Search Results Tests
370
+ // ==========================================================================
371
+
372
+ describe('Streaming Search Results', () => {
373
+ it('should stream large result sets', async () => {
374
+ const query = normalizedVector(384);
375
+ const k = 1000;
376
+
377
+ const stream = client.searchStream(query, {
378
+ k,
379
+ metric: 'cosine',
380
+ batchSize: 100,
381
+ });
382
+
383
+ const results: StreamSearchResult[] = [];
384
+
385
+ for await (const result of stream) {
386
+ results.push(result);
387
+ }
388
+
389
+ // Note: Limited by mock data size, should return up to min(k, dataSize)
390
+ expect(results.length).toBeGreaterThan(0);
391
+ expect(results.length).toBeLessThanOrEqual(k);
392
+ expect(results[0].score).toBeGreaterThanOrEqual(results[results.length - 1].score);
393
+ });
394
+
395
+ it('should respect batch size during streaming', async () => {
396
+ const query = normalizedVector(384);
397
+ const batchSize = 50;
398
+ const k = 200;
399
+
400
+ const stream = client.searchStream(query, {
401
+ k,
402
+ metric: 'cosine',
403
+ batchSize,
404
+ });
405
+
406
+ let totalResults = 0;
407
+
408
+ // Track batch reads through the cursor
409
+ const results: StreamSearchResult[] = [];
410
+
411
+ for await (const result of stream) {
412
+ results.push(result);
413
+ totalResults++;
414
+ }
415
+
416
+ // Results should be streamed in batches up to k items
417
+ expect(totalResults).toBeGreaterThan(0);
418
+ expect(totalResults).toBeLessThanOrEqual(k);
419
+ });
420
+
421
+ it('should include vectors when requested', async () => {
422
+ const query = normalizedVector(384);
423
+
424
+ const stream = client.searchStream(query, {
425
+ k: 10,
426
+ metric: 'cosine',
427
+ includeVector: true,
428
+ });
429
+
430
+ const results: StreamSearchResult[] = [];
431
+
432
+ for await (const result of stream) {
433
+ results.push(result);
434
+ }
435
+
436
+ results.forEach((r) => {
437
+ expect(r.vector).toBeDefined();
438
+ expect(r.vector).toHaveLength(384);
439
+ });
440
+ });
441
+
442
+ it('should allow filtering with transform stream', async () => {
443
+ const query = normalizedVector(384);
444
+ const minScore = 0.7;
445
+
446
+ const searchStream = client.searchStream(query, {
447
+ k: 100,
448
+ metric: 'cosine',
449
+ });
450
+
451
+ const filterStream = createScoreFilterTransform(minScore);
452
+
453
+ const results: StreamSearchResult[] = [];
454
+
455
+ await pipeline(
456
+ searchStream,
457
+ filterStream,
458
+ new Writable({
459
+ objectMode: true,
460
+ write(chunk, encoding, callback) {
461
+ results.push(chunk);
462
+ callback();
463
+ },
464
+ })
465
+ );
466
+
467
+ results.forEach((r) => {
468
+ expect(r.score).toBeGreaterThanOrEqual(minScore);
469
+ });
470
+ });
471
+
472
+ it('should support enrichment transforms', async () => {
473
+ const query = normalizedVector(384);
474
+
475
+ const searchStream = client.searchStream(query, {
476
+ k: 10,
477
+ metric: 'cosine',
478
+ });
479
+
480
+ const enrichStream = createEnrichmentTransform(async (result) => ({
481
+ ...result,
482
+ metadata: {
483
+ ...result.metadata,
484
+ enrichedAt: new Date().toISOString(),
485
+ source: 'ruvector',
486
+ },
487
+ }));
488
+
489
+ const results: StreamSearchResult[] = [];
490
+
491
+ await pipeline(
492
+ searchStream,
493
+ enrichStream,
494
+ new Writable({
495
+ objectMode: true,
496
+ write(chunk, encoding, callback) {
497
+ results.push(chunk);
498
+ callback();
499
+ },
500
+ })
501
+ );
502
+
503
+ results.forEach((r) => {
504
+ expect(r.metadata?.enrichedAt).toBeDefined();
505
+ expect(r.metadata?.source).toBe('ruvector');
506
+ });
507
+ });
508
+ });
509
+
510
+ // ==========================================================================
511
+ // Backpressure Handling Tests
512
+ // ==========================================================================
513
+
514
+ describe('Backpressure Handling', () => {
515
+ it('should handle backpressure from slow consumers', async () => {
516
+ const query = normalizedVector(384);
517
+
518
+ const searchStream = client.searchStream(query, {
519
+ k: 100,
520
+ metric: 'cosine',
521
+ batchSize: 10,
522
+ });
523
+
524
+ // Slow consumer - 5ms per item
525
+ const slowConsumer = createSlowConsumer(5);
526
+
527
+ let itemsProcessed = 0;
528
+
529
+ await pipeline(
530
+ searchStream,
531
+ new Transform({
532
+ objectMode: true,
533
+ transform(chunk, encoding, callback) {
534
+ itemsProcessed++;
535
+ this.push(chunk);
536
+ callback();
537
+ },
538
+ }),
539
+ slowConsumer
540
+ );
541
+
542
+ // Should process all items despite backpressure
543
+ expect(itemsProcessed).toBeGreaterThan(0);
544
+ expect(itemsProcessed).toBeLessThanOrEqual(100);
545
+ }, 10000); // Longer timeout for slow consumer
546
+
547
+ it('should not overwhelm memory with large result sets', async () => {
548
+ const query = normalizedVector(384);
549
+
550
+ // Get memory before streaming
551
+ const memBefore = process.memoryUsage().heapUsed;
552
+
553
+ const stream = client.searchStream(query, {
554
+ k: 5000,
555
+ metric: 'cosine',
556
+ batchSize: 50,
557
+ includeVector: true,
558
+ });
559
+
560
+ let count = 0;
561
+
562
+ for await (const result of stream) {
563
+ count++;
564
+ // Don't store results - just count them
565
+ }
566
+
567
+ const memAfter = process.memoryUsage().heapUsed;
568
+ const memDelta = memAfter - memBefore;
569
+
570
+ // Should process items (limited by mock data size of 10000)
571
+ expect(count).toBeGreaterThan(0);
572
+ expect(count).toBeLessThanOrEqual(5000);
573
+ // Memory should not grow excessively for streaming
574
+ expect(memDelta).toBeLessThan(100 * 1024 * 1024);
575
+ });
576
+
577
+ it('should pause and resume based on consumer speed', async () => {
578
+ const data: StreamSearchResult[] = Array.from({ length: 1000 }, (_, i) => ({
579
+ id: `item-${i}`,
580
+ score: 1 - i / 1000,
581
+ distance: i / 1000,
582
+ }));
583
+
584
+ const cursor = createCursor(data);
585
+ const stream = createSearchResultStream(cursor, 10);
586
+
587
+ let pauseCount = 0;
588
+ let resumeCount = 0;
589
+
590
+ stream.on('pause', () => pauseCount++);
591
+ stream.on('resume', () => resumeCount++);
592
+
593
+ // Variable speed consumer
594
+ let processed = 0;
595
+
596
+ await pipeline(
597
+ stream,
598
+ new Transform({
599
+ objectMode: true,
600
+ highWaterMark: 5, // Low watermark to trigger backpressure
601
+ async transform(chunk, encoding, callback) {
602
+ processed++;
603
+ // Occasionally slow down
604
+ if (processed % 100 === 0) {
605
+ await new Promise((r) => setTimeout(r, 10));
606
+ }
607
+ this.push(chunk);
608
+ callback();
609
+ },
610
+ }),
611
+ new Writable({
612
+ objectMode: true,
613
+ write(chunk, encoding, callback) {
614
+ callback();
615
+ },
616
+ })
617
+ );
618
+
619
+ expect(processed).toBe(1000);
620
+ });
621
+ });
622
+
623
+ // ==========================================================================
624
+ // Stream Batch Inserts Tests
625
+ // ==========================================================================
626
+
627
+ describe('Stream Batch Inserts', () => {
628
+ it('should stream batch inserts', async () => {
629
+ const vectorCount = 500;
630
+ const batchSize = 50;
631
+
632
+ const generator = createVectorGeneratorStream(vectorCount, 384);
633
+ const inserter = client.insertStream({
634
+ tableName: 'test_vectors',
635
+ batchSize,
636
+ });
637
+
638
+ await pipeline(generator, inserter);
639
+
640
+ expect(inserter.totalWritten).toBe(vectorCount);
641
+ expect(inserter.batchCount).toBe(Math.ceil(vectorCount / batchSize));
642
+ });
643
+
644
+ it('should handle partial final batch', async () => {
645
+ const vectorCount = 175; // Not divisible by batch size
646
+ const batchSize = 50;
647
+
648
+ const generator = createVectorGeneratorStream(vectorCount, 384);
649
+ const inserter = client.insertStream({
650
+ tableName: 'test_vectors',
651
+ batchSize,
652
+ });
653
+
654
+ await pipeline(generator, inserter);
655
+
656
+ expect(inserter.totalWritten).toBe(vectorCount);
657
+ expect(inserter.batchCount).toBe(4); // 50 + 50 + 50 + 25
658
+ });
659
+
660
+ it('should respect insert throughput', async () => {
661
+ const vectorCount = 1000;
662
+ const batchSize = 100;
663
+
664
+ const generator = createVectorGeneratorStream(vectorCount, 384);
665
+ const inserter = client.insertStream({
666
+ tableName: 'test_vectors',
667
+ batchSize,
668
+ });
669
+
670
+ const { durationMs } = await measureAsync(async () => {
671
+ await pipeline(generator, inserter);
672
+ });
673
+
674
+ const throughput = vectorCount / (durationMs / 1000);
675
+
676
+ expect(inserter.totalWritten).toBe(vectorCount);
677
+ expect(throughput).toBeGreaterThan(100); // At least 100 vectors/sec
678
+ });
679
+
680
+ it('should transform vectors before insert', async () => {
681
+ const vectorCount = 100;
682
+ const dimensions = 384;
683
+
684
+ const generator = createVectorGeneratorStream(vectorCount, dimensions, false);
685
+
686
+ // Normalize vectors before insert
687
+ const normalizer = new Transform({
688
+ objectMode: true,
689
+ transform(chunk, encoding, callback) {
690
+ const magnitude = Math.sqrt(
691
+ chunk.vector.reduce((sum: number, v: number) => sum + v * v, 0)
692
+ );
693
+ const normalized = {
694
+ ...chunk,
695
+ vector: chunk.vector.map((v: number) => v / magnitude),
696
+ metadata: { normalized: true },
697
+ };
698
+ this.push(normalized);
699
+ callback();
700
+ },
701
+ });
702
+
703
+ const insertedVectors: Array<{ id: string; vector: number[] }> = [];
704
+
705
+ const inserter = createBatchWriteStream(async (batch) => {
706
+ insertedVectors.push(...batch);
707
+ }, 25);
708
+
709
+ await pipeline(generator, normalizer, inserter);
710
+
711
+ expect(insertedVectors).toHaveLength(vectorCount);
712
+
713
+ // Check all vectors are normalized
714
+ insertedVectors.forEach(({ vector }) => {
715
+ const magnitude = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
716
+ expect(magnitude).toBeCloseTo(1, 5);
717
+ });
718
+ });
719
+
720
+ it('should handle insert errors gracefully', async () => {
721
+ const vectorCount = 100;
722
+ let errorTriggered = false;
723
+
724
+ const generator = createVectorGeneratorStream(vectorCount, 384);
725
+
726
+ const failingInserter = createBatchWriteStream(async (batch) => {
727
+ if (!errorTriggered && batch.length > 0) {
728
+ errorTriggered = true;
729
+ throw new Error('Simulated insert failure');
730
+ }
731
+ }, 50);
732
+
733
+ await expect(
734
+ pipeline(generator, failingInserter)
735
+ ).rejects.toThrow('Simulated insert failure');
736
+ });
737
+ });
738
+
739
+ // ==========================================================================
740
+ // Cursor Operations Tests
741
+ // ==========================================================================
742
+
743
+ describe('Cursor Operations', () => {
744
+ it('should create and iterate cursor', async () => {
745
+ const cursor = await client.createCursor('SELECT * FROM vectors');
746
+
747
+ const batches: Array<Record<string, unknown>[]> = [];
748
+ let batch: Record<string, unknown>[];
749
+
750
+ while ((batch = await cursor.read(100)).length > 0) {
751
+ batches.push(batch);
752
+ }
753
+
754
+ expect(batches.length).toBeGreaterThan(0);
755
+ expect(cursor.exhausted).toBe(true);
756
+
757
+ await cursor.close();
758
+ });
759
+
760
+ it('should support cursor with small batch size', async () => {
761
+ const cursor = await client.createCursor('SELECT * FROM vectors');
762
+
763
+ const items: Record<string, unknown>[] = [];
764
+ let batch: Record<string, unknown>[];
765
+
766
+ while ((batch = await cursor.read(10)).length > 0 && items.length < 50) {
767
+ items.push(...batch);
768
+ }
769
+
770
+ expect(items).toHaveLength(50);
771
+
772
+ await cursor.close();
773
+ });
774
+
775
+ it('should close cursor properly', async () => {
776
+ const cursor = await client.createCursor('SELECT * FROM vectors');
777
+
778
+ // Read some data
779
+ await cursor.read(50);
780
+ expect(cursor.exhausted).toBe(false);
781
+
782
+ // Close cursor
783
+ await cursor.close();
784
+ expect(cursor.exhausted).toBe(true);
785
+
786
+ // Further reads should return empty
787
+ const afterClose = await cursor.read(50);
788
+ expect(afterClose).toHaveLength(0);
789
+ });
790
+
791
+ it('should track cursor position', async () => {
792
+ const cursor = await client.createCursor('SELECT * FROM vectors');
793
+
794
+ expect(cursor.position).toBe(0);
795
+
796
+ await cursor.read(100);
797
+ expect(cursor.position).toBe(100);
798
+
799
+ await cursor.read(50);
800
+ expect(cursor.position).toBe(150);
801
+
802
+ await cursor.close();
803
+ });
804
+ });
805
+
806
+ // ==========================================================================
807
+ // Pipeline Composition Tests
808
+ // ==========================================================================
809
+
810
+ describe('Pipeline Composition', () => {
811
+ it('should compose multiple transforms', async () => {
812
+ const query = normalizedVector(384);
813
+
814
+ const searchStream = client.searchStream(query, {
815
+ k: 100,
816
+ metric: 'cosine',
817
+ });
818
+
819
+ // Filter by score
820
+ const filterTransform = createScoreFilterTransform(0.6);
821
+
822
+ // Enrich results
823
+ const enrichTransform = createEnrichmentTransform(async (result) => ({
824
+ ...result,
825
+ metadata: {
826
+ ...result.metadata,
827
+ processed: true,
828
+ },
829
+ }));
830
+
831
+ // Limit results
832
+ let count = 0;
833
+ const limitTransform = new Transform({
834
+ objectMode: true,
835
+ transform(chunk, encoding, callback) {
836
+ if (count < 20) {
837
+ this.push(chunk);
838
+ count++;
839
+ }
840
+ callback();
841
+ },
842
+ });
843
+
844
+ const results: StreamSearchResult[] = [];
845
+
846
+ await pipeline(
847
+ searchStream,
848
+ filterTransform,
849
+ enrichTransform,
850
+ limitTransform,
851
+ new Writable({
852
+ objectMode: true,
853
+ write(chunk, encoding, callback) {
854
+ results.push(chunk);
855
+ callback();
856
+ },
857
+ })
858
+ );
859
+
860
+ expect(results.length).toBeLessThanOrEqual(20);
861
+ results.forEach((r) => {
862
+ expect(r.score).toBeGreaterThanOrEqual(0.6);
863
+ expect(r.metadata?.processed).toBe(true);
864
+ });
865
+ });
866
+
867
+ it('should handle errors in pipeline', async () => {
868
+ const query = normalizedVector(384);
869
+
870
+ const searchStream = client.searchStream(query, {
871
+ k: 100,
872
+ metric: 'cosine',
873
+ });
874
+
875
+ const failingTransform = new Transform({
876
+ objectMode: true,
877
+ transform(chunk, encoding, callback) {
878
+ callback(new Error('Transform error'));
879
+ },
880
+ });
881
+
882
+ await expect(
883
+ pipeline(
884
+ searchStream,
885
+ failingTransform,
886
+ new Writable({
887
+ objectMode: true,
888
+ write(chunk, encoding, callback) {
889
+ callback();
890
+ },
891
+ })
892
+ )
893
+ ).rejects.toThrow('Transform error');
894
+ });
895
+
896
+ it('should support async generators', async () => {
897
+ async function* generateResults() {
898
+ for (let i = 0; i < 100; i++) {
899
+ yield {
900
+ id: `gen-${i}`,
901
+ vector: normalizedVector(384),
902
+ metadata: { index: i },
903
+ };
904
+ }
905
+ }
906
+
907
+ const results: Array<{ id: string }> = [];
908
+
909
+ for await (const item of generateResults()) {
910
+ results.push(item);
911
+ }
912
+
913
+ expect(results).toHaveLength(100);
914
+ });
915
+ });
916
+
917
+ // ==========================================================================
918
+ // Memory Efficiency Tests
919
+ // ==========================================================================
920
+
921
+ describe('Memory Efficiency', () => {
922
+ it('should process large datasets with constant memory', async () => {
923
+ const largeCount = 10000;
924
+ const batchSize = 100;
925
+
926
+ // Track memory at intervals
927
+ const memSnapshots: number[] = [];
928
+
929
+ const generator = createVectorGeneratorStream(largeCount, 384);
930
+
931
+ let processed = 0;
932
+
933
+ await pipeline(
934
+ generator,
935
+ new Transform({
936
+ objectMode: true,
937
+ transform(chunk, encoding, callback) {
938
+ processed++;
939
+
940
+ // Take memory snapshot every 1000 items
941
+ if (processed % 1000 === 0) {
942
+ memSnapshots.push(process.memoryUsage().heapUsed);
943
+ }
944
+
945
+ // Don't accumulate - just pass through
946
+ callback();
947
+ },
948
+ }),
949
+ new Writable({
950
+ objectMode: true,
951
+ write(chunk, encoding, callback) {
952
+ callback();
953
+ },
954
+ })
955
+ );
956
+
957
+ expect(processed).toBe(largeCount);
958
+
959
+ // Memory should not grow significantly
960
+ if (memSnapshots.length > 2) {
961
+ const firstSnapshot = memSnapshots[0];
962
+ const lastSnapshot = memSnapshots[memSnapshots.length - 1];
963
+ const growth = lastSnapshot - firstSnapshot;
964
+
965
+ // Allow up to 20MB growth
966
+ expect(growth).toBeLessThan(20 * 1024 * 1024);
967
+ }
968
+ });
969
+
970
+ it('should release references after streaming', async () => {
971
+ // Force GC if available
972
+ if (global.gc) {
973
+ global.gc();
974
+ }
975
+
976
+ const memBefore = process.memoryUsage().heapUsed;
977
+
978
+ // Process a large stream
979
+ const generator = createVectorGeneratorStream(5000, 384);
980
+
981
+ await pipeline(
982
+ generator,
983
+ new Writable({
984
+ objectMode: true,
985
+ write(chunk, encoding, callback) {
986
+ callback();
987
+ },
988
+ })
989
+ );
990
+
991
+ // Force GC if available
992
+ if (global.gc) {
993
+ global.gc();
994
+ }
995
+
996
+ const memAfter = process.memoryUsage().heapUsed;
997
+
998
+ // Memory growth should be reasonable (allow 50MB variance for test environment)
999
+ // In production with proper GC, this would be much lower
1000
+ expect(Math.abs(memAfter - memBefore)).toBeLessThan(50 * 1024 * 1024);
1001
+ });
1002
+ });
1003
+
1004
+ // ==========================================================================
1005
+ // Edge Cases
1006
+ // ==========================================================================
1007
+
1008
+ describe('Edge Cases', () => {
1009
+ it('should handle empty result stream', async () => {
1010
+ const cursor = createCursor<StreamSearchResult>([]);
1011
+ const stream = createSearchResultStream(cursor);
1012
+
1013
+ const results: StreamSearchResult[] = [];
1014
+
1015
+ for await (const result of stream) {
1016
+ results.push(result);
1017
+ }
1018
+
1019
+ expect(results).toHaveLength(0);
1020
+ });
1021
+
1022
+ it('should handle single result', async () => {
1023
+ const cursor = createCursor<StreamSearchResult>([
1024
+ { id: 'single', score: 1, distance: 0 },
1025
+ ]);
1026
+ const stream = createSearchResultStream(cursor);
1027
+
1028
+ const results: StreamSearchResult[] = [];
1029
+
1030
+ for await (const result of stream) {
1031
+ results.push(result);
1032
+ }
1033
+
1034
+ expect(results).toHaveLength(1);
1035
+ expect(results[0].id).toBe('single');
1036
+ });
1037
+
1038
+ it('should handle stream destruction', async () => {
1039
+ const cursor = createCursor<StreamSearchResult>(
1040
+ Array.from({ length: 1000 }, (_, i) => ({
1041
+ id: `item-${i}`,
1042
+ score: 1 - i / 1000,
1043
+ distance: i / 1000,
1044
+ }))
1045
+ );
1046
+
1047
+ const stream = createSearchResultStream(cursor, 10);
1048
+
1049
+ const results: StreamSearchResult[] = [];
1050
+
1051
+ for await (const result of stream) {
1052
+ results.push(result);
1053
+ if (results.length >= 50) {
1054
+ stream.destroy();
1055
+ break;
1056
+ }
1057
+ }
1058
+
1059
+ expect(results.length).toBeLessThanOrEqual(60); // May have some buffered
1060
+ });
1061
+
1062
+ it('should handle concurrent stream consumption', async () => {
1063
+ const query = normalizedVector(384);
1064
+
1065
+ // Create multiple concurrent streams
1066
+ const streams = Array.from({ length: 5 }, () =>
1067
+ client.searchStream(query, { k: 100, metric: 'cosine' })
1068
+ );
1069
+
1070
+ const results = await Promise.all(
1071
+ streams.map(async (stream) => {
1072
+ const items: StreamSearchResult[] = [];
1073
+ for await (const item of stream) {
1074
+ items.push(item);
1075
+ }
1076
+ return items;
1077
+ })
1078
+ );
1079
+
1080
+ expect(results).toHaveLength(5);
1081
+ results.forEach((r) => {
1082
+ // Each stream should return results (limited by mock data)
1083
+ expect(r.length).toBeGreaterThan(0);
1084
+ expect(r.length).toBeLessThanOrEqual(100);
1085
+ });
1086
+ });
1087
+ });
1088
+ });