@ekodb/ekodb-client 0.1.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/src/client.ts ADDED
@@ -0,0 +1,757 @@
1
+ /**
2
+ * ekoDB TypeScript Client
3
+ */
4
+
5
+ import { QueryBuilder, Query as QueryBuilderQuery } from './query-builder';
6
+ import { SearchQuery, SearchQueryBuilder, SearchResponse } from './search';
7
+ import { Schema, SchemaBuilder, CollectionMetadata } from './schema';
8
+
9
+ export interface Record {
10
+ [key: string]: any;
11
+ }
12
+
13
+ /**
14
+ * Rate limit information from the server
15
+ */
16
+ export interface RateLimitInfo {
17
+ /** Maximum requests allowed per window */
18
+ limit: number;
19
+ /** Requests remaining in current window */
20
+ remaining: number;
21
+ /** Unix timestamp when the rate limit resets */
22
+ reset: number;
23
+ }
24
+
25
+ /**
26
+ * Client configuration options
27
+ */
28
+ export interface ClientConfig {
29
+ /** Base URL of the ekoDB server */
30
+ baseURL: string;
31
+ /** API key for authentication */
32
+ apiKey: string;
33
+ /** Enable automatic retries for rate limiting and transient errors (default: true) */
34
+ shouldRetry?: boolean;
35
+ /** Maximum number of retry attempts (default: 3) */
36
+ maxRetries?: number;
37
+ /** Request timeout in milliseconds (default: 30000) */
38
+ timeout?: number;
39
+ }
40
+
41
+ /**
42
+ * Rate limit error
43
+ */
44
+ export class RateLimitError extends Error {
45
+ constructor(public retryAfterSecs: number, message?: string) {
46
+ super(message || `Rate limit exceeded. Retry after ${retryAfterSecs} seconds`);
47
+ this.name = 'RateLimitError';
48
+ }
49
+ }
50
+
51
+ export interface Query {
52
+ limit?: number;
53
+ offset?: number;
54
+ filter?: Record;
55
+ }
56
+
57
+ export interface BatchOperationResult {
58
+ successful: string[];
59
+ failed: Array<{ id?: string; error: string }>;
60
+ }
61
+
62
+ // ========== Chat Interfaces ==========
63
+
64
+ export interface CollectionConfig {
65
+ collection_name: string;
66
+ fields?: string[];
67
+ search_options?: any;
68
+ }
69
+
70
+ export interface ChatRequest {
71
+ collections: CollectionConfig[];
72
+ llm_provider: string;
73
+ llm_model?: string;
74
+ message: string;
75
+ system_prompt?: string;
76
+ bypass_ripple?: boolean;
77
+ }
78
+
79
+ export interface CreateChatSessionRequest {
80
+ collections: CollectionConfig[];
81
+ llm_provider: string;
82
+ llm_model?: string;
83
+ system_prompt?: string;
84
+ bypass_ripple?: boolean;
85
+ parent_id?: string;
86
+ branch_point_idx?: number;
87
+ max_context_messages?: number;
88
+ }
89
+
90
+ export interface ChatMessageRequest {
91
+ message: string;
92
+ bypass_ripple?: boolean;
93
+ force_summarize?: boolean;
94
+ }
95
+
96
+ export interface TokenUsage {
97
+ prompt_tokens: number;
98
+ completion_tokens: number;
99
+ total_tokens: number;
100
+ }
101
+
102
+ export interface ChatResponse {
103
+ chat_id: string;
104
+ message_id: string;
105
+ responses: string[];
106
+ context_snippets: any[];
107
+ execution_time_ms: number;
108
+ token_usage?: TokenUsage;
109
+ }
110
+
111
+ export interface ChatSession {
112
+ chat_id: string;
113
+ created_at: string;
114
+ updated_at: string;
115
+ llm_provider: string;
116
+ llm_model: string;
117
+ collections: CollectionConfig[];
118
+ system_prompt?: string;
119
+ title?: string;
120
+ message_count: number;
121
+ }
122
+
123
+ export interface ChatSessionResponse {
124
+ session: Record;
125
+ message_count: number;
126
+ }
127
+
128
+ export interface ListSessionsQuery {
129
+ limit?: number;
130
+ skip?: number;
131
+ sort?: string;
132
+ }
133
+
134
+ export interface ListSessionsResponse {
135
+ sessions: ChatSession[];
136
+ total: number;
137
+ returned: number;
138
+ skip: number;
139
+ limit?: number;
140
+ }
141
+
142
+ export interface GetMessagesQuery {
143
+ limit?: number;
144
+ skip?: number;
145
+ sort?: string;
146
+ }
147
+
148
+ export interface GetMessagesResponse {
149
+ messages: Record[];
150
+ total: number;
151
+ skip: number;
152
+ limit?: number;
153
+ returned: number;
154
+ }
155
+
156
+ export interface UpdateSessionRequest {
157
+ system_prompt?: string;
158
+ llm_model?: string;
159
+ collections?: CollectionConfig[];
160
+ max_context_messages?: number;
161
+ }
162
+
163
+ export enum MergeStrategy {
164
+ Chronological = 'Chronological',
165
+ Summarized = 'Summarized',
166
+ LatestOnly = 'LatestOnly',
167
+ }
168
+
169
+ export interface MergeSessionsRequest {
170
+ source_chat_ids: string[];
171
+ target_chat_id: string;
172
+ merge_strategy: MergeStrategy;
173
+ }
174
+
175
+ export class EkoDBClient {
176
+ private baseURL: string;
177
+ private apiKey: string;
178
+ private token: string | null = null;
179
+ private shouldRetry: boolean;
180
+ private maxRetries: number;
181
+ private timeout: number;
182
+ private rateLimitInfo: RateLimitInfo | null = null;
183
+
184
+ constructor(config: string | ClientConfig, apiKey?: string) {
185
+ // Support both old (baseURL, apiKey) and new (config object) signatures
186
+ if (typeof config === 'string') {
187
+ this.baseURL = config;
188
+ this.apiKey = apiKey!;
189
+ this.shouldRetry = true;
190
+ this.maxRetries = 3;
191
+ this.timeout = 30000;
192
+ } else {
193
+ this.baseURL = config.baseURL;
194
+ this.apiKey = config.apiKey;
195
+ this.shouldRetry = config.shouldRetry ?? true;
196
+ this.maxRetries = config.maxRetries ?? 3;
197
+ this.timeout = config.timeout ?? 30000;
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Initialize the client by getting an auth token
203
+ */
204
+ async init(): Promise<void> {
205
+ await this.refreshToken();
206
+ }
207
+
208
+ /**
209
+ * Get the current rate limit information
210
+ */
211
+ getRateLimitInfo(): RateLimitInfo | null {
212
+ return this.rateLimitInfo;
213
+ }
214
+
215
+ /**
216
+ * Check if approaching rate limit (less than 10% remaining)
217
+ */
218
+ isNearRateLimit(): boolean {
219
+ if (!this.rateLimitInfo) return false;
220
+ const threshold = this.rateLimitInfo.limit * 0.1;
221
+ return this.rateLimitInfo.remaining <= threshold;
222
+ }
223
+
224
+ /**
225
+ * Refresh the authentication token
226
+ */
227
+ private async refreshToken(): Promise<void> {
228
+ const response = await fetch(`${this.baseURL}/api/auth/token`, {
229
+ method: 'POST',
230
+ headers: { 'Content-Type': 'application/json' },
231
+ body: JSON.stringify({ api_key: this.apiKey }),
232
+ });
233
+
234
+ if (!response.ok) {
235
+ throw new Error(`Auth failed with status: ${response.status}`);
236
+ }
237
+
238
+ const result = await response.json() as { token: string };
239
+ this.token = result.token;
240
+ }
241
+
242
+ /**
243
+ * Extract rate limit information from response headers
244
+ */
245
+ private extractRateLimitInfo(response: Response): void {
246
+ const limit = response.headers.get('x-ratelimit-limit');
247
+ const remaining = response.headers.get('x-ratelimit-remaining');
248
+ const reset = response.headers.get('x-ratelimit-reset');
249
+
250
+ if (limit && remaining && reset) {
251
+ this.rateLimitInfo = {
252
+ limit: parseInt(limit, 10),
253
+ remaining: parseInt(remaining, 10),
254
+ reset: parseInt(reset, 10),
255
+ };
256
+
257
+ // Log warning if approaching rate limit
258
+ if (this.isNearRateLimit()) {
259
+ console.warn(
260
+ `Approaching rate limit: ${this.rateLimitInfo.remaining}/${this.rateLimitInfo.limit} remaining`
261
+ );
262
+ }
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Sleep for a specified number of seconds
268
+ */
269
+ private sleep(seconds: number): Promise<void> {
270
+ return new Promise(resolve => setTimeout(resolve, seconds * 1000));
271
+ }
272
+
273
+ /**
274
+ * Make an HTTP request to the ekoDB API with retry logic
275
+ */
276
+ private async makeRequest<T>(
277
+ method: string,
278
+ path: string,
279
+ data?: any,
280
+ attempt: number = 0
281
+ ): Promise<T> {
282
+ if (!this.token) {
283
+ await this.refreshToken();
284
+ }
285
+
286
+ const options: RequestInit = {
287
+ method,
288
+ headers: {
289
+ 'Authorization': `Bearer ${this.token}`,
290
+ 'Content-Type': 'application/json',
291
+ },
292
+ };
293
+
294
+ if (data) {
295
+ options.body = JSON.stringify(data);
296
+ }
297
+
298
+ try {
299
+ const response = await fetch(`${this.baseURL}${path}`, options);
300
+
301
+ // Extract rate limit info from successful responses
302
+ if (response.ok) {
303
+ this.extractRateLimitInfo(response);
304
+ return response.json() as Promise<T>;
305
+ }
306
+
307
+ // Handle rate limiting (429)
308
+ if (response.status === 429) {
309
+ const retryAfter = parseInt(response.headers.get('retry-after') || '60', 10);
310
+
311
+ if (this.shouldRetry && attempt < this.maxRetries) {
312
+ console.log(`Rate limited. Retrying after ${retryAfter} seconds...`);
313
+ await this.sleep(retryAfter);
314
+ return this.makeRequest<T>(method, path, data, attempt + 1);
315
+ }
316
+
317
+ throw new RateLimitError(retryAfter);
318
+ }
319
+
320
+ // Handle service unavailable (503)
321
+ if (response.status === 503 && this.shouldRetry && attempt < this.maxRetries) {
322
+ const retryDelay = 10;
323
+ console.log(`Service unavailable. Retrying after ${retryDelay} seconds...`);
324
+ await this.sleep(retryDelay);
325
+ return this.makeRequest<T>(method, path, data, attempt + 1);
326
+ }
327
+
328
+ // Handle other errors
329
+ const text = await response.text();
330
+ throw new Error(`Request failed with status ${response.status}: ${text}`);
331
+ } catch (error) {
332
+ // Handle network errors with retry
333
+ if (error instanceof TypeError && this.shouldRetry && attempt < this.maxRetries) {
334
+ const retryDelay = 3;
335
+ console.log(`Network error. Retrying after ${retryDelay} seconds...`);
336
+ await this.sleep(retryDelay);
337
+ return this.makeRequest<T>(method, path, data, attempt + 1);
338
+ }
339
+
340
+ throw error;
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Insert a document into a collection
346
+ */
347
+ async insert(collection: string, record: Record, ttl?: string): Promise<Record> {
348
+ const data = { ...record };
349
+ if (ttl) {
350
+ data.ttl_duration = ttl;
351
+ }
352
+ return this.makeRequest<Record>('POST', `/api/insert/${collection}`, data);
353
+ }
354
+
355
+ /**
356
+ * Find documents in a collection
357
+ *
358
+ * @param collection - Collection name
359
+ * @param query - Query object or QueryBuilder instance
360
+ * @returns Array of matching records
361
+ *
362
+ * @example
363
+ * ```typescript
364
+ * // Using QueryBuilder
365
+ * const results = await client.find("users",
366
+ * new QueryBuilder()
367
+ * .eq("status", "active")
368
+ * .gt("age", 18)
369
+ * .sortDesc("created_at")
370
+ * .limit(10)
371
+ * );
372
+ *
373
+ * // Using plain Query object
374
+ * const results = await client.find("users", { limit: 10 });
375
+ * ```
376
+ */
377
+ async find(collection: string, query: Query | QueryBuilder = {}): Promise<Record[]> {
378
+ const queryObj = query instanceof QueryBuilder ? query.build() : query;
379
+ return this.makeRequest<Record[]>('POST', `/api/find/${collection}`, queryObj);
380
+ }
381
+
382
+ /**
383
+ * Find a document by ID
384
+ */
385
+ async findByID(collection: string, id: string): Promise<Record> {
386
+ return this.makeRequest<Record>('GET', `/api/find/${collection}/${id}`);
387
+ }
388
+
389
+ /**
390
+ * Update a document
391
+ */
392
+ async update(collection: string, id: string, record: Record): Promise<Record> {
393
+ return this.makeRequest<Record>('PUT', `/api/update/${collection}/${id}`, record);
394
+ }
395
+
396
+ /**
397
+ * Delete a document
398
+ */
399
+ async delete(collection: string, id: string): Promise<void> {
400
+ await this.makeRequest<void>('DELETE', `/api/delete/${collection}/${id}`);
401
+ }
402
+
403
+ /**
404
+ * Batch insert multiple documents
405
+ */
406
+ async batchInsert(collection: string, records: Record[]): Promise<Record[]> {
407
+ const inserts = records.map(data => ({ data }));
408
+ const result = await this.makeRequest<BatchOperationResult>(
409
+ 'POST',
410
+ `/api/batch/insert/${collection}`,
411
+ { inserts }
412
+ );
413
+ return result.successful.map(id => ({ id }));
414
+ }
415
+
416
+ /**
417
+ * Batch update multiple documents
418
+ */
419
+ async batchUpdate(
420
+ collection: string,
421
+ updates: Array<{ id: string; data: Record }>
422
+ ): Promise<Record[]> {
423
+ const result = await this.makeRequest<BatchOperationResult>(
424
+ 'PUT',
425
+ `/api/batch/update/${collection}`,
426
+ { updates }
427
+ );
428
+ return result.successful.map(id => ({ id }));
429
+ }
430
+
431
+ /**
432
+ * Batch delete multiple documents
433
+ */
434
+ async batchDelete(collection: string, ids: string[]): Promise<number> {
435
+ const deletes = ids.map(id => ({ id }));
436
+ const result = await this.makeRequest<BatchOperationResult>(
437
+ 'DELETE',
438
+ `/api/batch/delete/${collection}`,
439
+ { deletes }
440
+ );
441
+ return result.successful.length;
442
+ }
443
+
444
+ /**
445
+ * Set a key-value pair
446
+ */
447
+ async kvSet(key: string, value: any): Promise<void> {
448
+ await this.makeRequest<void>('POST', `/api/kv/set/${encodeURIComponent(key)}`, { value });
449
+ }
450
+
451
+ /**
452
+ * Get a value by key
453
+ */
454
+ async kvGet(key: string): Promise<any> {
455
+ const result = await this.makeRequest<{ value: any }>(
456
+ 'GET',
457
+ `/api/kv/get/${encodeURIComponent(key)}`
458
+ );
459
+ return result.value;
460
+ }
461
+
462
+ /**
463
+ * Delete a key
464
+ */
465
+ async kvDelete(key: string): Promise<void> {
466
+ await this.makeRequest<void>('DELETE', `/api/kv/delete/${encodeURIComponent(key)}`);
467
+ }
468
+
469
+ /**
470
+ * List all collections
471
+ */
472
+ async listCollections(): Promise<string[]> {
473
+ const result = await this.makeRequest<{ collections: string[] }>('GET', '/api/collections');
474
+ return result.collections;
475
+ }
476
+
477
+ /**
478
+ * Delete a collection
479
+ */
480
+ async deleteCollection(collection: string): Promise<void> {
481
+ await this.makeRequest<void>('DELETE', `/api/collections/${collection}`);
482
+ }
483
+
484
+ /**
485
+ * Create a collection with schema
486
+ *
487
+ * @param collection - Collection name
488
+ * @param schema - Schema definition or SchemaBuilder instance
489
+ *
490
+ * @example
491
+ * ```typescript
492
+ * const schema = new SchemaBuilder()
493
+ * .addField("name", new FieldTypeSchemaBuilder("string").required())
494
+ * .addField("email", new FieldTypeSchemaBuilder("string").unique())
495
+ * .addField("age", new FieldTypeSchemaBuilder("number").range(0, 150));
496
+ *
497
+ * await client.createCollection("users", schema);
498
+ * ```
499
+ */
500
+ async createCollection(collection: string, schema: Schema | SchemaBuilder): Promise<void> {
501
+ const schemaObj = schema instanceof SchemaBuilder ? schema.build() : schema;
502
+ await this.makeRequest<void>('POST', `/api/collections/${collection}`, schemaObj);
503
+ }
504
+
505
+ /**
506
+ * Get collection metadata and schema
507
+ *
508
+ * @param collection - Collection name
509
+ * @returns Collection metadata including schema and analytics
510
+ */
511
+ async getCollection(collection: string): Promise<CollectionMetadata> {
512
+ return this.makeRequest<CollectionMetadata>('GET', `/api/collections/${collection}`);
513
+ }
514
+
515
+ /**
516
+ * Get collection schema
517
+ *
518
+ * @param collection - Collection name
519
+ * @returns The collection schema
520
+ */
521
+ async getSchema(collection: string): Promise<Schema> {
522
+ const metadata = await this.getCollection(collection);
523
+ return metadata.collection;
524
+ }
525
+
526
+ /**
527
+ * Search documents in a collection using full-text, vector, or hybrid search
528
+ *
529
+ * @param collection - Collection name
530
+ * @param searchQuery - Search query object or SearchQueryBuilder instance
531
+ * @returns Search response with results and metadata
532
+ *
533
+ * @example
534
+ * ```typescript
535
+ * // Full-text search
536
+ * const results = await client.search("users",
537
+ * new SearchQueryBuilder("john")
538
+ * .fields(["name", "email"])
539
+ * .fuzzy(true)
540
+ * .limit(10)
541
+ * );
542
+ *
543
+ * // Vector search
544
+ * const results = await client.search("documents",
545
+ * new SearchQueryBuilder("")
546
+ * .vector([0.1, 0.2, 0.3, ...])
547
+ * .vectorK(5)
548
+ * );
549
+ *
550
+ * // Hybrid search
551
+ * const results = await client.search("products",
552
+ * new SearchQueryBuilder("laptop")
553
+ * .vector([0.1, 0.2, ...])
554
+ * .textWeight(0.7)
555
+ * .vectorWeight(0.3)
556
+ * );
557
+ * ```
558
+ */
559
+ async search(collection: string, searchQuery: SearchQuery | SearchQueryBuilder): Promise<SearchResponse> {
560
+ const queryObj = searchQuery instanceof SearchQueryBuilder ? searchQuery.build() : searchQuery;
561
+ return this.makeRequest<SearchResponse>('POST', `/api/search/${collection}`, queryObj);
562
+ }
563
+
564
+ // ========== Chat Methods ==========
565
+
566
+ /**
567
+ * Create a new chat session
568
+ */
569
+ async createChatSession(request: CreateChatSessionRequest): Promise<ChatResponse> {
570
+ return this.makeRequest<ChatResponse>('POST', '/api/chat', request);
571
+ }
572
+
573
+ /**
574
+ * Send a message in an existing chat session
575
+ */
576
+ async chatMessage(sessionId: string, request: ChatMessageRequest): Promise<ChatResponse> {
577
+ return this.makeRequest<ChatResponse>('POST', `/api/chat/${sessionId}/messages`, request);
578
+ }
579
+
580
+ /**
581
+ * Get a chat session by ID
582
+ */
583
+ async getChatSession(sessionId: string): Promise<ChatSessionResponse> {
584
+ return this.makeRequest<ChatSessionResponse>('GET', `/api/chat/${sessionId}`);
585
+ }
586
+
587
+ /**
588
+ * List all chat sessions
589
+ */
590
+ async listChatSessions(query?: ListSessionsQuery): Promise<ListSessionsResponse> {
591
+ const params = new URLSearchParams();
592
+ if (query?.limit) params.append('limit', query.limit.toString());
593
+ if (query?.skip) params.append('skip', query.skip.toString());
594
+ if (query?.sort) params.append('sort', query.sort);
595
+
596
+ const queryString = params.toString();
597
+ const path = queryString ? `/api/chat?${queryString}` : '/api/chat';
598
+ return this.makeRequest<ListSessionsResponse>('GET', path);
599
+ }
600
+
601
+ /**
602
+ * Get messages from a chat session
603
+ */
604
+ async getChatSessionMessages(sessionId: string, query?: GetMessagesQuery): Promise<GetMessagesResponse> {
605
+ const params = new URLSearchParams();
606
+ if (query?.limit) params.append('limit', query.limit.toString());
607
+ if (query?.skip) params.append('skip', query.skip.toString());
608
+ if (query?.sort) params.append('sort', query.sort);
609
+
610
+ const queryString = params.toString();
611
+ const path = queryString ? `/api/chat/${sessionId}/messages?${queryString}` : `/api/chat/${sessionId}/messages`;
612
+ return this.makeRequest<GetMessagesResponse>('GET', path);
613
+ }
614
+
615
+ /**
616
+ * Update a chat session
617
+ */
618
+ async updateChatSession(sessionId: string, request: UpdateSessionRequest): Promise<ChatSessionResponse> {
619
+ return this.makeRequest<ChatSessionResponse>('PUT', `/api/chat/${sessionId}`, request);
620
+ }
621
+
622
+ /**
623
+ * Branch a chat session
624
+ */
625
+ async branchChatSession(request: CreateChatSessionRequest): Promise<ChatResponse> {
626
+ return this.makeRequest<ChatResponse>('POST', '/api/chat/branch', request);
627
+ }
628
+
629
+ /**
630
+ * Delete a chat session
631
+ */
632
+ async deleteChatSession(sessionId: string): Promise<void> {
633
+ await this.makeRequest<void>('DELETE', `/api/chat/${sessionId}`);
634
+ }
635
+
636
+ /**
637
+ * Regenerate an AI response message
638
+ */
639
+ async regenerateMessage(sessionId: string, messageId: string): Promise<ChatResponse> {
640
+ return this.makeRequest<ChatResponse>('POST', `/api/chat/${sessionId}/messages/${messageId}/regenerate`);
641
+ }
642
+
643
+ /**
644
+ * Update a specific message
645
+ */
646
+ async updateChatMessage(sessionId: string, messageId: string, content: string): Promise<void> {
647
+ await this.makeRequest<void>('PUT', `/api/chat/${sessionId}/messages/${messageId}`, { content });
648
+ }
649
+
650
+ /**
651
+ * Delete a specific message
652
+ */
653
+ async deleteChatMessage(sessionId: string, messageId: string): Promise<void> {
654
+ await this.makeRequest<void>('DELETE', `/api/chat/${sessionId}/messages/${messageId}`);
655
+ }
656
+
657
+ /**
658
+ * Toggle the "forgotten" status of a message
659
+ */
660
+ async toggleForgottenMessage(sessionId: string, messageId: string, forgotten: boolean): Promise<void> {
661
+ await this.makeRequest<void>('PATCH', `/api/chat/${sessionId}/messages/${messageId}/forgotten`, { forgotten });
662
+ }
663
+
664
+ /**
665
+ * Merge multiple chat sessions into one
666
+ */
667
+ async mergeChatSessions(request: MergeSessionsRequest): Promise<ChatSessionResponse> {
668
+ return this.makeRequest<ChatSessionResponse>('POST', '/api/chat/merge', request);
669
+ }
670
+
671
+ /**
672
+ * Create a WebSocket client
673
+ */
674
+ websocket(wsURL: string): WebSocketClient {
675
+ return new WebSocketClient(wsURL, this.token!);
676
+ }
677
+ }
678
+
679
+ /**
680
+ * WebSocket client for real-time queries
681
+ */
682
+ export class WebSocketClient {
683
+ private wsURL: string;
684
+ private token: string;
685
+ private ws: any = null;
686
+
687
+ constructor(wsURL: string, token: string) {
688
+ this.wsURL = wsURL;
689
+ this.token = token;
690
+ }
691
+
692
+ /**
693
+ * Connect to WebSocket
694
+ */
695
+ private async connect(): Promise<void> {
696
+ if (this.ws) return;
697
+
698
+ // Dynamic import for Node.js WebSocket
699
+ const WebSocket = (await import('ws')).default;
700
+
701
+ let url = this.wsURL;
702
+ if (!url.endsWith('/api/ws')) {
703
+ url += '/api/ws';
704
+ }
705
+
706
+ this.ws = new WebSocket(url, {
707
+ headers: {
708
+ 'Authorization': `Bearer ${this.token}`,
709
+ },
710
+ });
711
+
712
+ return new Promise((resolve, reject) => {
713
+ this.ws.on('open', () => resolve());
714
+ this.ws.on('error', (err: Error) => reject(err));
715
+ });
716
+ }
717
+
718
+ /**
719
+ * Find all records in a collection via WebSocket
720
+ */
721
+ async findAll(collection: string): Promise<Record[]> {
722
+ await this.connect();
723
+
724
+ const messageId = Date.now().toString();
725
+ const request = {
726
+ type: 'FindAll',
727
+ messageId,
728
+ payload: { collection },
729
+ };
730
+
731
+ return new Promise((resolve, reject) => {
732
+ this.ws.send(JSON.stringify(request));
733
+
734
+ this.ws.once('message', (data: Buffer) => {
735
+ const response = JSON.parse(data.toString());
736
+
737
+ if (response.type === 'Error') {
738
+ reject(new Error(response.message));
739
+ } else {
740
+ resolve(response.payload?.data || []);
741
+ }
742
+ });
743
+
744
+ this.ws.once('error', reject);
745
+ });
746
+ }
747
+
748
+ /**
749
+ * Close the WebSocket connection
750
+ */
751
+ close(): void {
752
+ if (this.ws) {
753
+ this.ws.close();
754
+ this.ws = null;
755
+ }
756
+ }
757
+ }