@ekodb/ekodb-client 0.23.0 → 0.24.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/dist/client.d.ts +14 -0
- package/dist/client.js +27 -1
- package/dist/client.test.js +26 -0
- package/package.json +2 -2
- package/src/client.test.ts +41 -1
- package/src/client.ts +36 -0
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 &&
|
package/dist/client.test.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Official TypeScript/JavaScript client for ekoDB",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"author": "ekoDB",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@types/node": "^
|
|
23
|
+
"@types/node": "^24.13.2",
|
|
24
24
|
"@types/ws": "^8.18.1",
|
|
25
25
|
"typescript": "^5.9.3",
|
|
26
26
|
"vitest": "^4.0.18"
|
package/src/client.test.ts
CHANGED
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
|
|
9
|
-
import {
|
|
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 &&
|