@ekodb/ekodb-client 0.23.0 → 0.23.1

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/dist/client.d.ts CHANGED
@@ -31,6 +31,12 @@ export interface RateLimitInfo {
31
31
  /**
32
32
  * Client configuration options
33
33
  */
34
+ /**
35
+ * Default per-request timeout (ms) for REST calls, matching the Rust and Go
36
+ * clients' 30s default. Applied to every non-streaming request; long-lived
37
+ * streaming/SSE calls are intentionally not bounded by it.
38
+ */
39
+ export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
34
40
  export interface ClientConfig {
35
41
  /** Base URL of the ekoDB server */
36
42
  baseURL: string;
@@ -42,6 +48,13 @@ export interface ClientConfig {
42
48
  maxRetries?: number;
43
49
  /** Serialization format (default: MessagePack for best performance, use Json for debugging) */
44
50
  format?: SerializationFormat;
51
+ /**
52
+ * Per-request timeout in milliseconds for REST calls (default: 30000).
53
+ * A slow or unreachable server aborts with a timeout error instead of hanging
54
+ * the caller. Streaming/SSE calls are not bounded by it. Matches the Rust and
55
+ * Go clients.
56
+ */
57
+ timeout?: number;
45
58
  }
46
59
  /**
47
60
  * Rate limit error
@@ -342,6 +355,7 @@ export declare class EkoDBClient {
342
355
  private shouldRetry;
343
356
  private maxRetries;
344
357
  private format;
358
+ private requestTimeoutMs;
345
359
  private rateLimitInfo;
346
360
  constructor(config: string | ClientConfig, apiKey?: string);
347
361
  /**
package/dist/client.js CHANGED
@@ -36,7 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  };
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.WebSocketClient = exports.SchemaCache = exports.EventStream = exports.EkoDBClient = exports.MergeStrategy = exports.RateLimitError = exports.SerializationFormat = void 0;
39
+ exports.WebSocketClient = exports.SchemaCache = exports.EventStream = exports.EkoDBClient = exports.MergeStrategy = exports.RateLimitError = exports.DEFAULT_REQUEST_TIMEOUT_MS = exports.SerializationFormat = void 0;
40
40
  exports.extractRecordId = extractRecordId;
41
41
  const msgpack_1 = require("@msgpack/msgpack");
42
42
  const query_builder_1 = require("./query-builder");
@@ -51,6 +51,15 @@ var SerializationFormat;
51
51
  /** MessagePack format (binary, 2-3x faster) */
52
52
  SerializationFormat["MessagePack"] = "MessagePack";
53
53
  })(SerializationFormat || (exports.SerializationFormat = SerializationFormat = {}));
54
+ /**
55
+ * Client configuration options
56
+ */
57
+ /**
58
+ * Default per-request timeout (ms) for REST calls, matching the Rust and Go
59
+ * clients' 30s default. Applied to every non-streaming request; long-lived
60
+ * streaming/SSE calls are intentionally not bounded by it.
61
+ */
62
+ exports.DEFAULT_REQUEST_TIMEOUT_MS = 30000;
54
63
  /**
55
64
  * Rate limit error
56
65
  */
@@ -96,6 +105,7 @@ class EkoDBClient {
96
105
  this.shouldRetry = true;
97
106
  this.maxRetries = 3;
98
107
  this.format = SerializationFormat.MessagePack; // Default to MessagePack for 2-3x performance
108
+ this.requestTimeoutMs = exports.DEFAULT_REQUEST_TIMEOUT_MS;
99
109
  }
100
110
  else {
101
111
  this.baseURL = stripTrailingSlashes(config.baseURL);
@@ -103,6 +113,7 @@ class EkoDBClient {
103
113
  this.shouldRetry = config.shouldRetry ?? true;
104
114
  this.maxRetries = config.maxRetries ?? 3;
105
115
  this.format = config.format ?? SerializationFormat.MessagePack; // Default to MessagePack for 2-3x performance
116
+ this.requestTimeoutMs = config.timeout ?? exports.DEFAULT_REQUEST_TIMEOUT_MS;
106
117
  }
107
118
  }
108
119
  /**
@@ -134,6 +145,7 @@ class EkoDBClient {
134
145
  method: "POST",
135
146
  headers: { "Content-Type": "application/json" },
136
147
  body: JSON.stringify({ api_key: this.apiKey }),
148
+ signal: AbortSignal.timeout(this.requestTimeoutMs),
137
149
  });
138
150
  if (!response.ok) {
139
151
  let errorMsg = `Auth failed with status: ${response.status}`;
@@ -317,6 +329,10 @@ class EkoDBClient {
317
329
  "Content-Type": contentType,
318
330
  Accept: contentType,
319
331
  },
332
+ // Bound every REST request so a slow/unreachable server aborts instead of
333
+ // hanging the caller. A fresh signal per attempt is correct here because
334
+ // makeRequest recurses on retry, rebuilding `options` each time.
335
+ signal: AbortSignal.timeout(this.requestTimeoutMs),
320
336
  };
321
337
  if (data) {
322
338
  options.body = isMessagePack
@@ -371,6 +387,16 @@ class EkoDBClient {
371
387
  throw new Error(`Request failed with status ${response.status}: ${text}`);
372
388
  }
373
389
  catch (error) {
390
+ // A fired request-timeout (AbortSignal.timeout) surfaces as a DOMException
391
+ // named "TimeoutError" (or "AbortError" in some runtimes). Convert it to a
392
+ // clear error and do NOT retry — retrying would multiply the wait beyond
393
+ // the configured bound.
394
+ const errName = error && typeof error === "object" && "name" in error
395
+ ? error.name
396
+ : undefined;
397
+ if (errName === "TimeoutError" || errName === "AbortError") {
398
+ throw new Error(`Request timed out after ${this.requestTimeoutMs}ms: ${method} ${path}`);
399
+ }
374
400
  // Handle network errors with retry
375
401
  if (error instanceof TypeError &&
376
402
  this.shouldRetry &&
@@ -2645,3 +2645,29 @@ function mockErrorResponse(status, message) {
2645
2645
  (0, vitest_1.expect)(url).toContain("/api/chat/sess%231/messages");
2646
2646
  });
2647
2647
  });
2648
+ (0, vitest_1.describe)("request timeout", () => {
2649
+ (0, vitest_1.it)("defaults to a 30s request timeout", () => {
2650
+ (0, vitest_1.expect)(client_1.DEFAULT_REQUEST_TIMEOUT_MS).toBe(30000);
2651
+ });
2652
+ (0, vitest_1.it)("aborts a request that exceeds the configured timeout", async () => {
2653
+ const client = new client_1.EkoDBClient({
2654
+ baseURL: "http://localhost:8080",
2655
+ apiKey: "test-api-key",
2656
+ format: client_1.SerializationFormat.Json,
2657
+ shouldRetry: false,
2658
+ timeout: 20,
2659
+ });
2660
+ // Token fetch resolves immediately...
2661
+ mockTokenResponse();
2662
+ // ...but the actual request hangs until the request-timeout signal aborts
2663
+ // it. The mock honors the AbortSignal the client attaches, rejecting with
2664
+ // its reason (a "TimeoutError" DOMException) exactly like real fetch.
2665
+ mockFetch.mockImplementationOnce((_url, opts) => new Promise((_resolve, reject) => {
2666
+ const signal = opts.signal;
2667
+ signal?.addEventListener("abort", () => {
2668
+ reject(signal.reason ?? new DOMException("timed out", "TimeoutError"));
2669
+ });
2670
+ }));
2671
+ await (0, vitest_1.expect)(client.find("users")).rejects.toThrow(/timed out after 20ms/);
2672
+ });
2673
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekodb/ekodb-client",
3
- "version": "0.23.0",
3
+ "version": "0.23.1",
4
4
  "description": "Official TypeScript/JavaScript client for ekoDB",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -6,7 +6,12 @@
6
6
  */
7
7
 
8
8
  import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
9
- import { EkoDBClient, SerializationFormat, extractRecordId } from "./client";
9
+ import {
10
+ EkoDBClient,
11
+ SerializationFormat,
12
+ extractRecordId,
13
+ DEFAULT_REQUEST_TIMEOUT_MS,
14
+ } from "./client";
10
15
  import { SearchQueryBuilder } from "./search";
11
16
 
12
17
  // Mock fetch globally
@@ -3478,3 +3483,38 @@ describe("EkoDBClient URL path segment encoding", () => {
3478
3483
  expect(url as string).toContain("/api/chat/sess%231/messages");
3479
3484
  });
3480
3485
  });
3486
+
3487
+ describe("request timeout", () => {
3488
+ it("defaults to a 30s request timeout", () => {
3489
+ expect(DEFAULT_REQUEST_TIMEOUT_MS).toBe(30000);
3490
+ });
3491
+
3492
+ it("aborts a request that exceeds the configured timeout", async () => {
3493
+ const client = new EkoDBClient({
3494
+ baseURL: "http://localhost:8080",
3495
+ apiKey: "test-api-key",
3496
+ format: SerializationFormat.Json,
3497
+ shouldRetry: false,
3498
+ timeout: 20,
3499
+ });
3500
+
3501
+ // Token fetch resolves immediately...
3502
+ mockTokenResponse();
3503
+ // ...but the actual request hangs until the request-timeout signal aborts
3504
+ // it. The mock honors the AbortSignal the client attaches, rejecting with
3505
+ // its reason (a "TimeoutError" DOMException) exactly like real fetch.
3506
+ mockFetch.mockImplementationOnce(
3507
+ (_url: string, opts: RequestInit) =>
3508
+ new Promise((_resolve, reject) => {
3509
+ const signal = opts.signal as AbortSignal | undefined;
3510
+ signal?.addEventListener("abort", () => {
3511
+ reject(
3512
+ signal.reason ?? new DOMException("timed out", "TimeoutError"),
3513
+ );
3514
+ });
3515
+ }),
3516
+ );
3517
+
3518
+ await expect(client.find("users")).rejects.toThrow(/timed out after 20ms/);
3519
+ });
3520
+ });
package/src/client.ts CHANGED
@@ -37,6 +37,13 @@ export interface RateLimitInfo {
37
37
  /**
38
38
  * Client configuration options
39
39
  */
40
+ /**
41
+ * Default per-request timeout (ms) for REST calls, matching the Rust and Go
42
+ * clients' 30s default. Applied to every non-streaming request; long-lived
43
+ * streaming/SSE calls are intentionally not bounded by it.
44
+ */
45
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
46
+
40
47
  export interface ClientConfig {
41
48
  /** Base URL of the ekoDB server */
42
49
  baseURL: string;
@@ -48,6 +55,13 @@ export interface ClientConfig {
48
55
  maxRetries?: number;
49
56
  /** Serialization format (default: MessagePack for best performance, use Json for debugging) */
50
57
  format?: SerializationFormat;
58
+ /**
59
+ * Per-request timeout in milliseconds for REST calls (default: 30000).
60
+ * A slow or unreachable server aborts with a timeout error instead of hanging
61
+ * the caller. Streaming/SSE calls are not bounded by it. Matches the Rust and
62
+ * Go clients.
63
+ */
64
+ timeout?: number;
51
65
  }
52
66
 
53
67
  /**
@@ -419,6 +433,7 @@ export class EkoDBClient {
419
433
  private shouldRetry: boolean;
420
434
  private maxRetries: number;
421
435
  private format: SerializationFormat;
436
+ private requestTimeoutMs: number;
422
437
  private rateLimitInfo: RateLimitInfo | null = null;
423
438
 
424
439
  constructor(config: string | ClientConfig, apiKey?: string) {
@@ -431,12 +446,14 @@ export class EkoDBClient {
431
446
  this.shouldRetry = true;
432
447
  this.maxRetries = 3;
433
448
  this.format = SerializationFormat.MessagePack; // Default to MessagePack for 2-3x performance
449
+ this.requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
434
450
  } else {
435
451
  this.baseURL = stripTrailingSlashes(config.baseURL);
436
452
  this.apiKey = config.apiKey;
437
453
  this.shouldRetry = config.shouldRetry ?? true;
438
454
  this.maxRetries = config.maxRetries ?? 3;
439
455
  this.format = config.format ?? SerializationFormat.MessagePack; // Default to MessagePack for 2-3x performance
456
+ this.requestTimeoutMs = config.timeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
440
457
  }
441
458
  }
442
459
 
@@ -471,6 +488,7 @@ export class EkoDBClient {
471
488
  method: "POST",
472
489
  headers: { "Content-Type": "application/json" },
473
490
  body: JSON.stringify({ api_key: this.apiKey }),
491
+ signal: AbortSignal.timeout(this.requestTimeoutMs),
474
492
  });
475
493
 
476
494
  if (!response.ok) {
@@ -683,6 +701,10 @@ export class EkoDBClient {
683
701
  "Content-Type": contentType,
684
702
  Accept: contentType,
685
703
  },
704
+ // Bound every REST request so a slow/unreachable server aborts instead of
705
+ // hanging the caller. A fresh signal per attempt is correct here because
706
+ // makeRequest recurses on retry, rebuilding `options` each time.
707
+ signal: AbortSignal.timeout(this.requestTimeoutMs),
686
708
  };
687
709
 
688
710
  if (data) {
@@ -757,6 +779,20 @@ export class EkoDBClient {
757
779
  const text = await response.text();
758
780
  throw new Error(`Request failed with status ${response.status}: ${text}`);
759
781
  } catch (error) {
782
+ // A fired request-timeout (AbortSignal.timeout) surfaces as a DOMException
783
+ // named "TimeoutError" (or "AbortError" in some runtimes). Convert it to a
784
+ // clear error and do NOT retry — retrying would multiply the wait beyond
785
+ // the configured bound.
786
+ const errName =
787
+ error && typeof error === "object" && "name" in error
788
+ ? (error as { name?: string }).name
789
+ : undefined;
790
+ if (errName === "TimeoutError" || errName === "AbortError") {
791
+ throw new Error(
792
+ `Request timed out after ${this.requestTimeoutMs}ms: ${method} ${path}`,
793
+ );
794
+ }
795
+
760
796
  // Handle network errors with retry
761
797
  if (
762
798
  error instanceof TypeError &&