@ekodb/ekodb-client 0.19.0 → 0.21.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/README.md CHANGED
@@ -254,6 +254,7 @@ const joinResults = await client.find("users", multiQuery);
254
254
  - `kvSet(key: string, value: any): Promise<void>`
255
255
  - `kvGet(key: string): Promise<any>`
256
256
  - `kvDelete(key: string): Promise<void>`
257
+ - `kvClear(): Promise<void>` - Clear the entire KV store
257
258
 
258
259
  #### Query Builder
259
260
 
@@ -269,7 +270,6 @@ const joinResults = await client.find("users", multiQuery);
269
270
  - `.contains(field, value)` - String contains
270
271
  - `.startsWith(field, value)` - String starts with
271
272
  - `.endsWith(field, value)` - String ends with
272
- - `.regex(field, pattern)` - Regex match
273
273
  - `.sortAsc(field)` / `.sortDesc(field)` - Sorting
274
274
  - `.limit(n)` / `.skip(n)` - Pagination
275
275
  - `.join(joinConfig)` - Add join configuration
@@ -298,6 +298,8 @@ const joinResults = await client.find("users", multiQuery);
298
298
  #### Collection Management
299
299
 
300
300
  - `listCollections(): Promise<string[]>`
301
+ - `listUserCollections(): Promise<string[]>` - List collections excluding
302
+ internal chat/system collections
301
303
  - `deleteCollection(collection: string): Promise<void>`
302
304
  - `collectionExists(collection: string): Promise<boolean>` - Check if collection
303
305
  exists
@@ -347,6 +349,14 @@ const joinResults = await client.find("users", multiQuery);
347
349
  - `deleteCollection(name): Promise<void>`
348
350
  - `close(): void`
349
351
 
352
+ **Subscriptions & chat:**
353
+
354
+ - `subscribe(collection, options?): Promise<EventStream<MutationNotification>>`
355
+ - Subscribe to a collection's mutation notifications
356
+ - `unsubscribe(collection): void` - Tear down a subscription (not replayed on
357
+ reconnect)
358
+ - `cancelChat(chatId): Promise<void>` - Cancel an in-flight streaming chat
359
+
350
360
  **Schema Cache:**
351
361
 
352
362
  ```typescript
@@ -360,6 +370,23 @@ const id = extractRecordId(record); // tries "id", "_id"
360
370
  const id2 = ws.extractId("users", record); // uses cache
361
371
  ```
362
372
 
373
+ ### Transactions
374
+
375
+ Buffered, read-your-writes transactions. Statements issued with a
376
+ `transactionId` are staged and applied atomically at commit.
377
+
378
+ - `beginTransaction(isolationLevel?): Promise<string>` - Start a transaction and
379
+ return its id
380
+ - `commitTransaction(transactionId): Promise<void>` - Apply staged writes. May
381
+ reject with a retryable conflict (HTTP 409)
382
+ - `rollbackTransaction(transactionId): Promise<void>` - Discard staged writes
383
+ - `createSavepoint(transactionId, name): Promise<void>`
384
+ - `rollbackToSavepoint(transactionId, name): Promise<void>`
385
+ - `releaseSavepoint(transactionId, name): Promise<void>`
386
+
387
+ Pass `transactionId` in the options of `insert` / `update` / `delete` / `find` /
388
+ `findById` to read and write within the transaction (read-your-writes).
389
+
363
390
  ## Examples
364
391
 
365
392
  See the
package/dist/client.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * ekoDB TypeScript Client
3
3
  */
4
- import { QueryBuilder } from "./query-builder";
4
+ import { QueryBuilder, Query } from "./query-builder";
5
5
  import { SearchQuery, SearchResponse } from "./search";
6
6
  import { Schema, SchemaBuilder, CollectionMetadata } from "./schema";
7
7
  import { UserFunction, FunctionResult } from "./functions";
@@ -50,11 +50,7 @@ export declare class RateLimitError extends Error {
50
50
  retryAfterSecs: number;
51
51
  constructor(retryAfterSecs: number, message?: string);
52
52
  }
53
- export interface Query {
54
- limit?: number;
55
- offset?: number;
56
- filter?: Record;
57
- }
53
+ export type { Query };
58
54
  export interface BatchOperationResult {
59
55
  successful: string[];
60
56
  failed: Array<{
@@ -95,6 +91,23 @@ export interface FindOptions {
95
91
  bypassCache?: boolean;
96
92
  selectFields?: string[];
97
93
  excludeFields?: string[];
94
+ /**
95
+ * Read within a transaction (read-your-writes). When set, the read is served
96
+ * from the transaction's own view — its uncommitted staged writes, else the
97
+ * committed store — and recorded in the transaction's read set for
98
+ * commit-time conflict detection. Omit for an ordinary committed read.
99
+ */
100
+ transactionId?: string;
101
+ }
102
+ /**
103
+ * Options for a point read by id. `transactionId` enables read-your-writes
104
+ * within a transaction (see {@link FindOptions.transactionId}).
105
+ */
106
+ export interface FindByIdOptions {
107
+ selectFields?: string[];
108
+ excludeFields?: string[];
109
+ bypassRipple?: boolean;
110
+ transactionId?: string;
98
111
  }
99
112
  export interface BatchInsertOptions {
100
113
  bypassRipple?: boolean;
@@ -374,6 +387,20 @@ export declare class EkoDBClient {
374
387
  * Sleep for a specified number of seconds
375
388
  */
376
389
  private sleep;
390
+ /**
391
+ * Parse a `Retry-After` header into a non-negative delay in seconds.
392
+ *
393
+ * Per RFC 9110 the value is either delay-seconds (an integer) or an
394
+ * HTTP-date. Anything that doesn't resolve to a finite, non-negative number
395
+ * (missing header, garbage, a past date) falls back to `defaultSecs`.
396
+ */
397
+ private parseRetryAfter;
398
+ /**
399
+ * Backoff delay (in seconds) for a 0-indexed retry attempt: a capped
400
+ * exponential schedule (0.2s → 5s) with full jitter, so concurrent clients
401
+ * don't retry in lockstep. Returns a value in [d/2, d].
402
+ */
403
+ private backoffSeconds;
377
404
  /**
378
405
  * Helper to determine if a path should use JSON
379
406
  * Only CRUD operations (insert/update/delete/batch) use MessagePack
@@ -413,19 +440,25 @@ export declare class EkoDBClient {
413
440
  * const results = await client.find("users", { limit: 10 });
414
441
  * ```
415
442
  */
416
- find(collection: string, query?: Query | QueryBuilder): Promise<Record[]>;
443
+ find(collection: string, query?: Query | QueryBuilder, options?: {
444
+ bypassRipple?: boolean;
445
+ transactionId?: string;
446
+ }): Promise<Record[]>;
417
447
  /**
418
- * Find a document by ID
448
+ * Find a document by ID.
449
+ * @param options - Optional read options. `transactionId` reads within a
450
+ * transaction (read-your-writes); see {@link FindByIdOptions}.
419
451
  */
420
- findById(collection: string, id: string): Promise<Record>;
452
+ findById(collection: string, id: string, options?: FindByIdOptions): Promise<Record>;
421
453
  /**
422
454
  * Find a document by ID with field projection
423
455
  * @param collection - Collection name
424
456
  * @param id - Document ID
425
457
  * @param selectFields - Fields to include in the result
426
458
  * @param excludeFields - Fields to exclude from the result
459
+ * @param transactionId - Read within a transaction (read-your-writes)
427
460
  */
428
- findByIdWithProjection(collection: string, id: string, selectFields?: string[], excludeFields?: string[]): Promise<Record>;
461
+ findByIdWithProjection(collection: string, id: string, selectFields?: string[], excludeFields?: string[], transactionId?: string): Promise<Record>;
429
462
  /**
430
463
  * Update a document
431
464
  * @param collection - Collection name
@@ -500,6 +533,10 @@ export declare class EkoDBClient {
500
533
  * Delete a key
501
534
  */
502
535
  kvDelete(key: string): Promise<void>;
536
+ /**
537
+ * Clear the entire KV store (all keys in the namespace).
538
+ */
539
+ kvClear(): Promise<void>;
503
540
  /**
504
541
  * Batch get multiple keys
505
542
  * @param keys - Array of keys to retrieve
@@ -546,7 +583,18 @@ export declare class EkoDBClient {
546
583
  include_expired?: boolean;
547
584
  }): Promise<any[]>;
548
585
  /**
549
- * Begin a new transaction
586
+ * Begin a new transaction.
587
+ *
588
+ * Transactions are buffered: statements issued with this `transactionId`
589
+ * (passed via the `transactionId` option on insert/update/delete/find/…) are
590
+ * staged and applied atomically only at {@link commitTransaction}. They are
591
+ * invisible to everyone else until commit, and visible to this transaction's
592
+ * own reads (read-your-writes) only when those reads also carry the
593
+ * `transactionId`. {@link rollbackTransaction} discards the staged writes.
594
+ * `commitTransaction` may reject with a conflict (HTTP 409) if a record this
595
+ * transaction read or wrote was changed by another committed transaction —
596
+ * retry the transaction in that case.
597
+ *
550
598
  * @param isolationLevel - Transaction isolation level (default: "ReadCommitted")
551
599
  * @returns Transaction ID
552
600
  */
@@ -566,10 +614,23 @@ export declare class EkoDBClient {
566
614
  */
567
615
  commitTransaction(transactionId: string): Promise<void>;
568
616
  /**
569
- * Rollback a transaction
617
+ * Rollback a transaction (discards all staged writes; nothing was applied).
570
618
  * @param transactionId - The transaction ID to rollback
571
619
  */
572
620
  rollbackTransaction(transactionId: string): Promise<void>;
621
+ /**
622
+ * Create a savepoint within a transaction. A later
623
+ * {@link rollbackToSavepoint} discards everything staged after it.
624
+ */
625
+ createSavepoint(transactionId: string, name: string): Promise<void>;
626
+ /**
627
+ * Roll the transaction back to a savepoint, discarding writes staged after it.
628
+ */
629
+ rollbackToSavepoint(transactionId: string, name: string): Promise<void>;
630
+ /**
631
+ * Release (forget) a savepoint. Staged work is unaffected.
632
+ */
633
+ releaseSavepoint(transactionId: string, name: string): Promise<void>;
573
634
  /**
574
635
  * Insert or update a record (upsert operation)
575
636
  *
@@ -658,6 +719,10 @@ export declare class EkoDBClient {
658
719
  * List all collections
659
720
  */
660
721
  listCollections(): Promise<string[]>;
722
+ /**
723
+ * List collections, excluding internal chat/system collections.
724
+ */
725
+ listUserCollections(): Promise<string[]>;
661
726
  /**
662
727
  * Delete a collection
663
728
  */
@@ -1090,9 +1155,17 @@ export declare class EkoDBClient {
1090
1155
  filterValue?: string;
1091
1156
  }): EventStream<MutationNotification>;
1092
1157
  /**
1093
- * Create a WebSocket client
1158
+ * Create a WebSocket client.
1159
+ *
1160
+ * The token is supplied as a provider bound to this client's
1161
+ * {@link getToken}, so every (re)connect re-evaluates (and proactively
1162
+ * refreshes) the auth token instead of snapshotting it once. This means a
1163
+ * reconnect after a token rotation uses the current token.
1164
+ *
1165
+ * @param wsURL - The WebSocket URL (e.g. `wss://host`); `/api/ws` is appended if absent.
1166
+ * @param options - Optional reconnect/timeout tunables.
1094
1167
  */
1095
- websocket(wsURL: string): WebSocketClient;
1168
+ websocket(wsURL: string, options?: WebSocketClientOptions): WebSocketClient;
1096
1169
  /**
1097
1170
  * Generate embeddings for a single text
1098
1171
  *
@@ -1236,6 +1309,35 @@ export interface SubscribeOptions {
1236
1309
  filterField?: string;
1237
1310
  filterValue?: string;
1238
1311
  }
1312
+ /**
1313
+ * A token provider: either a static token string, or a (possibly async)
1314
+ * function that returns a fresh token. When a function is supplied it is
1315
+ * re-invoked on every (re)connect, so a rotated/refreshed token is always
1316
+ * used for the new socket instead of a stale snapshot captured once.
1317
+ */
1318
+ export type TokenProvider = string | (() => string | null | Promise<string | null>);
1319
+ /** Tunables for the WebSocket client's reconnect + request-timeout behavior. */
1320
+ export interface WebSocketClientOptions {
1321
+ /**
1322
+ * Auto-reconnect after an unexpected socket close/error (not an explicit
1323
+ * `close()`/unsubscribe). Defaults to true.
1324
+ */
1325
+ autoReconnect?: boolean;
1326
+ /** Initial backoff delay in ms before the first reconnect attempt. Default 200. */
1327
+ reconnectInitialDelayMs?: number;
1328
+ /** Maximum backoff delay in ms (the cap for exponential growth). Default 5000. */
1329
+ reconnectMaxDelayMs?: number;
1330
+ /**
1331
+ * Maximum number of consecutive reconnect attempts before giving up.
1332
+ * 0 or undefined means unlimited. Default unlimited.
1333
+ */
1334
+ reconnectMaxAttempts?: number;
1335
+ /**
1336
+ * Per-request timeout in ms for request/response WS calls. If no response
1337
+ * arrives in this window the pending promise rejects. Default 30000.
1338
+ */
1339
+ requestTimeoutMs?: number;
1340
+ }
1239
1341
  /** EventEmitter-like interface for subscriptions and chat streams. */
1240
1342
  export declare class EventStream<_T = unknown> {
1241
1343
  private listeners;
@@ -1280,24 +1382,89 @@ export declare class SchemaCache {
1280
1382
  export declare function extractRecordId(record: Record, extraCandidates?: string[]): string | undefined;
1281
1383
  export declare class WebSocketClient {
1282
1384
  private wsURL;
1283
- private token;
1385
+ private tokenProvider;
1284
1386
  private ws;
1285
1387
  private dispatcherRunning;
1286
1388
  private schemaCache;
1389
+ /**
1390
+ * Per-connection wire format, set by negotiateFormat() on every (re)connect:
1391
+ * true once the server has Welcomed msgpack, so frames are sent/received as
1392
+ * binary msgpack; false (JSON text) otherwise, including against an older
1393
+ * server that never Welcomes. Keeps the transport fully back-compatible.
1394
+ */
1395
+ private binary;
1396
+ private autoReconnect;
1397
+ private reconnectInitialDelayMs;
1398
+ private reconnectMaxDelayMs;
1399
+ private reconnectMaxAttempts;
1400
+ private requestTimeoutMs;
1401
+ /** Set while close() is in progress so the close handler doesn't reconnect. */
1402
+ private closed;
1403
+ private reconnectAttempts;
1404
+ private reconnecting;
1405
+ private connectPromise;
1287
1406
  private pendingRequests;
1288
1407
  private subscriptions;
1408
+ /** Bookkeeping so subscriptions can be replayed on reconnect. */
1409
+ private subscriptionParams;
1289
1410
  private chatStreams;
1290
1411
  private registerToolsAck;
1291
- constructor(wsURL: string, token: string);
1412
+ /**
1413
+ * @param wsURL - WebSocket URL; `/api/ws` is appended if absent.
1414
+ * @param token - A static token string OR a {@link TokenProvider} function
1415
+ * re-evaluated on every (re)connect (so a refreshed token is used after a drop).
1416
+ * @param options - Optional reconnect/timeout tunables.
1417
+ */
1418
+ constructor(wsURL: string, token: TokenProvider, options?: WebSocketClientOptions);
1292
1419
  private messageCounter;
1293
1420
  private genMessageId;
1294
1421
  /**
1295
- * Connect and start the dispatcher.
1422
+ * Compute the capped exponential backoff (with jitter) for a reconnect
1423
+ * attempt. attempt 0 -> ~initial, growing x2 each time up to the max cap.
1424
+ * Jitter is +/-25% to avoid thundering-herd reconnect storms.
1425
+ * @internal exposed for testing
1426
+ */
1427
+ computeBackoff(attempt: number): number;
1428
+ /**
1429
+ * Connect and start the dispatcher. Re-evaluates the token provider so the
1430
+ * current/refreshed token is used for this socket.
1296
1431
  */
1297
1432
  private ensureConnected;
1433
+ private openSocket;
1434
+ /**
1435
+ * Additive capability handshake: offer msgpack and, if the server Welcomes
1436
+ * it, switch this connection to binary msgpack frames; otherwise stay on JSON
1437
+ * text. The Welcome (a text frame) is read with a one-shot listener and a
1438
+ * timeout so an older server that never answers — or answers with an Error —
1439
+ * simply leaves the connection on JSON. Best-effort and never throws: JSON
1440
+ * always works.
1441
+ */
1442
+ private negotiateFormat;
1443
+ /**
1444
+ * Send a request object on the active socket using the negotiated format:
1445
+ * binary msgpack when the server Welcomed it, JSON text otherwise. The single
1446
+ * write point so every request honors the negotiated transport.
1447
+ */
1448
+ private sendFrame;
1298
1449
  private spawnDispatcher;
1450
+ /**
1451
+ * Reject in-flight requests and tear down the dead socket. If the close was
1452
+ * unexpected (not an explicit `close()`) and auto-reconnect is enabled,
1453
+ * schedule a reconnect that re-sends the active subscriptions.
1454
+ */
1455
+ private handleDisconnect;
1456
+ /**
1457
+ * Reconnect with capped exponential backoff + jitter, then re-send the
1458
+ * subscribe messages for every active subscription so the SAME EventStream
1459
+ * keeps delivering mutations after a transient drop.
1460
+ */
1461
+ private scheduleReconnect;
1462
+ /** Re-send Subscribe frames for every tracked subscription after a reconnect. */
1463
+ private resubscribeAll;
1299
1464
  private routeMessage;
1300
1465
  private sendRequest;
1466
+ /** Resolve/reject a pending request, clearing its timeout timer. */
1467
+ private settlePending;
1301
1468
  /**
1302
1469
  * Find all records in a collection via WebSocket.
1303
1470
  */
@@ -1307,6 +1474,11 @@ export declare class WebSocketClient {
1307
1474
  * Returns an EventStream that emits "mutation" events.
1308
1475
  */
1309
1476
  subscribe(collection: string, options?: SubscribeOptions): Promise<EventStream<MutationNotification>>;
1477
+ /**
1478
+ * Unsubscribe from a collection's mutation notifications. This is an
1479
+ * intentional teardown, so the subscription is NOT replayed on reconnect.
1480
+ */
1481
+ unsubscribe(collection: string): void;
1310
1482
  /**
1311
1483
  * Send a chat message and receive a streaming response.
1312
1484
  * Returns an EventStream that emits "event" with ChatStreamEvent objects.
@@ -1320,6 +1492,11 @@ export declare class WebSocketClient {
1320
1492
  * Send a tool result back to the server during a chat stream.
1321
1493
  */
1322
1494
  sendToolResult(chatId: string, callId: string, success: boolean, result?: any, error?: string): Promise<void>;
1495
+ /**
1496
+ * Cancel an in-flight streaming chat. Fire-and-forget: tells the server to
1497
+ * stop generating tokens for the given chat.
1498
+ */
1499
+ cancelChat(chatId: string): Promise<void>;
1323
1500
  /**
1324
1501
  * Stateless raw LLM completion via WebSocket.
1325
1502
  *
@@ -1369,6 +1546,10 @@ export declare class WebSocketClient {
1369
1546
  deleteCollection(name: string): Promise<void>;
1370
1547
  /**
1371
1548
  * Close the WebSocket connection.
1549
+ *
1550
+ * This is an INTENTIONAL close: it disables auto-reconnect, rejects any
1551
+ * in-flight requests, and tears down all subscriptions/chat streams so
1552
+ * nothing is replayed afterward.
1372
1553
  */
1373
1554
  close(): void;
1374
1555
  }