@ekodb/ekodb-client 0.18.2 → 0.20.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.
@@ -489,6 +489,41 @@ function mockErrorResponse(status, message) {
489
489
  mockJsonResponse({ status: "deleted" });
490
490
  await (0, vitest_1.expect)(client.deleteChatSession("chat_123")).resolves.not.toThrow();
491
491
  });
492
+ (0, vitest_1.it)("compacts chat history", async () => {
493
+ const client = createTestClient();
494
+ mockTokenResponse();
495
+ mockJsonResponse({
496
+ folded: 12,
497
+ kept_recent: 8,
498
+ summary_chars: 1024,
499
+ summary_message_id: "msg_summary_001",
500
+ already_compact: false,
501
+ });
502
+ const result = await client.compactChat("chat_123", 8);
503
+ (0, vitest_1.expect)(result).toHaveProperty("folded", 12);
504
+ (0, vitest_1.expect)(result).toHaveProperty("kept_recent", 8);
505
+ (0, vitest_1.expect)(result).toHaveProperty("summary_message_id", "msg_summary_001");
506
+ (0, vitest_1.expect)(result).toHaveProperty("already_compact", false);
507
+ const [, init] = mockFetch.mock.calls[1];
508
+ (0, vitest_1.expect)(init.method).toBe("POST");
509
+ (0, vitest_1.expect)(JSON.parse(init.body)).toEqual({ keep_recent: 8 });
510
+ });
511
+ (0, vitest_1.it)("compacts chat history without keepRecent", async () => {
512
+ const client = createTestClient();
513
+ mockTokenResponse();
514
+ mockJsonResponse({
515
+ folded: 0,
516
+ kept_recent: 0,
517
+ summary_chars: 0,
518
+ summary_message_id: null,
519
+ already_compact: true,
520
+ });
521
+ const result = await client.compactChat("chat_123");
522
+ (0, vitest_1.expect)(result.already_compact).toBe(true);
523
+ (0, vitest_1.expect)(result.summary_message_id).toBeNull();
524
+ const [, init] = mockFetch.mock.calls[1];
525
+ (0, vitest_1.expect)(JSON.parse(init.body)).toEqual({});
526
+ });
492
527
  });
493
528
  // ============================================================================
494
529
  // Error Handling Tests
@@ -1781,6 +1816,69 @@ function mockErrorResponse(status, message) {
1781
1816
  (0, vitest_1.expect)(dataCall[1]?.method).toBe("POST");
1782
1817
  (0, vitest_1.expect)(dataCall[1]?.headers?.Accept).toBe("text/event-stream");
1783
1818
  });
1819
+ (0, vitest_1.it)("sends a resolved Bearer token, not a Promise (regression #124)", async () => {
1820
+ const client = createTestClient();
1821
+ mockTokenResponse();
1822
+ mockFetch.mockResolvedValueOnce({
1823
+ ok: true,
1824
+ status: 200,
1825
+ text: async () => 'data: {"token":"ok"}\n',
1826
+ headers: new Headers({ "content-type": "text/event-stream" }),
1827
+ });
1828
+ client.chatMessageStream("chat_789", { message: "Test" });
1829
+ await new Promise((resolve) => setTimeout(resolve, 50));
1830
+ const calls = global.fetch.mock.calls;
1831
+ const auth = calls[1][1]?.headers
1832
+ ?.Authorization;
1833
+ (0, vitest_1.expect)(auth).toBe("Bearer test-jwt-token");
1834
+ (0, vitest_1.expect)(auth).not.toContain("[object Promise]");
1835
+ });
1836
+ (0, vitest_1.it)("streams SSE events incrementally from response.body, reassembling split lines (regression #125)", async () => {
1837
+ const client = createTestClient();
1838
+ mockTokenResponse();
1839
+ // A data line is deliberately split across chunk boundaries to exercise the
1840
+ // incremental buffer (the old code buffered the whole body via text()).
1841
+ const enc = new TextEncoder();
1842
+ const chunks = [
1843
+ 'data: {"token":"He"}\nda',
1844
+ 'ta: {"token":"llo"}\n',
1845
+ 'data: {"content":"Hello","message_id":"m1","execution_time_ms":1}\n',
1846
+ ];
1847
+ const body = new ReadableStream({
1848
+ start(controller) {
1849
+ for (const c of chunks)
1850
+ controller.enqueue(enc.encode(c));
1851
+ controller.close();
1852
+ },
1853
+ });
1854
+ mockFetch.mockResolvedValueOnce({
1855
+ ok: true,
1856
+ status: 200,
1857
+ body,
1858
+ text: async () => chunks.join(""),
1859
+ headers: new Headers({ "content-type": "text/event-stream" }),
1860
+ });
1861
+ const events = [];
1862
+ const stream = client.chatMessageStream("chat_s", { message: "Hi" });
1863
+ stream.on("event", (evt) => events.push(evt));
1864
+ await new Promise((resolve) => setTimeout(resolve, 50));
1865
+ (0, vitest_1.expect)(events.filter((e) => e.type === "chunk").map((e) => e.content)).toEqual(["He", "llo"]);
1866
+ (0, vitest_1.expect)(events[events.length - 1].type).toBe("end");
1867
+ (0, vitest_1.expect)(events[events.length - 1].messageId).toBe("m1");
1868
+ });
1869
+ });
1870
+ (0, vitest_1.describe)("EkoDBClient retry backoff", () => {
1871
+ (0, vitest_1.it)("backoffSeconds grows, caps at 5s, and jitters within [d/2, d] (#126)", () => {
1872
+ const client = createTestClient();
1873
+ for (let attempt = 0; attempt < 10; attempt++) {
1874
+ const d = Math.min(0.2 * 2 ** attempt, 5);
1875
+ for (let i = 0; i < 50; i++) {
1876
+ const v = client.backoffSeconds(attempt);
1877
+ (0, vitest_1.expect)(v).toBeGreaterThanOrEqual(d / 2);
1878
+ (0, vitest_1.expect)(v).toBeLessThanOrEqual(d);
1879
+ }
1880
+ }
1881
+ });
1784
1882
  });
1785
1883
  // ============================================================================
1786
1884
  // Schedule CRUD Tests
@@ -2224,3 +2322,57 @@ function mockErrorResponse(status, message) {
2224
2322
  (0, vitest_1.expect)(errors[0]).toContain("401");
2225
2323
  });
2226
2324
  });
2325
+ (0, vitest_1.describe)("baseURL normalization", () => {
2326
+ (0, vitest_1.it)("strips a trailing slash so request URLs have no double slash", async () => {
2327
+ mockTokenResponse();
2328
+ const client = new client_1.EkoDBClient({
2329
+ baseURL: "http://localhost:8080/",
2330
+ apiKey: "test-api-key",
2331
+ format: client_1.SerializationFormat.Json,
2332
+ shouldRetry: false,
2333
+ });
2334
+ await client.init();
2335
+ const url = mockFetch.mock.calls[0][0];
2336
+ (0, vitest_1.expect)(url).toBe("http://localhost:8080/api/auth/token");
2337
+ (0, vitest_1.expect)(url).not.toContain("//api");
2338
+ });
2339
+ (0, vitest_1.it)("leaves a URL without a trailing slash unchanged", async () => {
2340
+ mockTokenResponse();
2341
+ const client = new client_1.EkoDBClient({
2342
+ baseURL: "http://localhost:8080",
2343
+ apiKey: "test-api-key",
2344
+ format: client_1.SerializationFormat.Json,
2345
+ shouldRetry: false,
2346
+ });
2347
+ await client.init();
2348
+ const url = mockFetch.mock.calls[0][0];
2349
+ (0, vitest_1.expect)(url).toBe("http://localhost:8080/api/auth/token");
2350
+ });
2351
+ });
2352
+ (0, vitest_1.describe)("extractRecordId", () => {
2353
+ (0, vitest_1.it)("returns a plain string id", () => {
2354
+ (0, vitest_1.expect)((0, client_1.extractRecordId)({ id: "abc" })).toBe("abc");
2355
+ });
2356
+ (0, vitest_1.it)("unwraps a genuine typed wrapper id", () => {
2357
+ (0, vitest_1.expect)((0, client_1.extractRecordId)({ id: { type: "String", value: "abc" } })).toBe("abc");
2358
+ });
2359
+ (0, vitest_1.it)("stringifies a wrapped numeric id", () => {
2360
+ (0, vitest_1.expect)((0, client_1.extractRecordId)({ id: { type: "Integer", value: 123 } })).toBe("123");
2361
+ });
2362
+ (0, vitest_1.it)("does not treat a user object with a value key (no type) as the id", () => {
2363
+ // Regression for #134: { value: 1, currency: "USD" } is a user object,
2364
+ // not a typed wrapper, so it must not be unwrapped into the id.
2365
+ (0, vitest_1.expect)((0, client_1.extractRecordId)({ id: { value: 1, currency: "USD" } })).toBeUndefined();
2366
+ });
2367
+ (0, vitest_1.it)("prefers an alias candidate over id", () => {
2368
+ (0, vitest_1.expect)((0, client_1.extractRecordId)({ users_id: "u1", id: "x" }, ["users_id"])).toBe("u1");
2369
+ });
2370
+ (0, vitest_1.it)("ignores a non-wrapper alias object and falls back to id", () => {
2371
+ (0, vitest_1.expect)((0, client_1.extractRecordId)({ users_id: { value: 7, label: "lvl" }, id: "real" }, [
2372
+ "users_id",
2373
+ ])).toBe("real");
2374
+ });
2375
+ (0, vitest_1.it)("falls back to _id", () => {
2376
+ (0, vitest_1.expect)((0, client_1.extractRecordId)({ _id: "underscore" })).toBe("underscore");
2377
+ });
2378
+ });
@@ -3,7 +3,6 @@
3
3
  *
4
4
  * These tests cover the pure-data construction helpers and the structural
5
5
  * parameter placeholder. They don't hit a running ekoDB — server-side
6
- * behavior is covered by the Rust integration tests in
7
- * `ekodb/ekodb_server/tests/function_parameters_tests.rs`.
6
+ * behavior is covered by the server-side integration tests.
8
7
  */
9
8
  export {};
@@ -4,8 +4,7 @@
4
4
  *
5
5
  * These tests cover the pure-data construction helpers and the structural
6
6
  * parameter placeholder. They don't hit a running ekoDB — server-side
7
- * behavior is covered by the Rust integration tests in
8
- * `ekodb/ekodb_server/tests/function_parameters_tests.rs`.
7
+ * behavior is covered by the server-side integration tests.
9
8
  */
10
9
  Object.defineProperty(exports, "__esModule", { value: true });
11
10
  const vitest_1 = require("vitest");
package/dist/index.d.ts CHANGED
@@ -12,4 +12,4 @@ export type { Schema, FieldTypeSchema, IndexConfig, CollectionMetadata, } from "
12
12
  export type { JoinConfig } from "./join";
13
13
  export type { UserFunction, ParameterDefinition, FunctionStageConfig, GroupFunctionConfig, SortFieldConfig, FunctionResult, FunctionStats, StageStats, } from "./functions";
14
14
  export type { MutationNotification, ChatStreamEvent, ClientToolDefinition, ChatSendOptions, SubscribeOptions, } from "./client";
15
- export type { Record, Query, BatchOperationResult, ClientConfig, RateLimitInfo, CollectionConfig, ChatRequest, CreateChatSessionRequest, ChatMessageRequest, TokenUsage, ChatResponse, ChatSession, ChatSessionResponse, ListSessionsQuery, ListSessionsResponse, GetMessagesQuery, GetMessagesResponse, UpdateSessionRequest, MergeSessionsRequest, ChatModels, EmbedRequest, EmbedResponse, RawCompletionRequest, RawCompletionResponse, ToolChoice, ToolConfig, } from "./client";
15
+ export type { Record, Query, BatchOperationResult, ClientConfig, RateLimitInfo, CollectionConfig, ChatRequest, CreateChatSessionRequest, ChatMessageRequest, TokenUsage, ChatResponse, ChatSession, ChatSessionResponse, ListSessionsQuery, ListSessionsResponse, GetMessagesQuery, GetMessagesResponse, UpdateSessionRequest, MergeSessionsRequest, ChatModels, CompactChatRequest, CompactChatResponse, EmbedRequest, EmbedResponse, RawCompletionRequest, RawCompletionResponse, ToolChoice, ToolConfig, } from "./client";
@@ -86,10 +86,6 @@ export declare class QueryBuilder {
86
86
  * Add an ends-with filter
87
87
  */
88
88
  endsWith(field: string, suffix: string): this;
89
- /**
90
- * Add a regex filter
91
- */
92
- regex(field: string, pattern: string): this;
93
89
  /**
94
90
  * Combine filters with AND logic
95
91
  */
@@ -189,20 +189,8 @@ class QueryBuilder {
189
189
  });
190
190
  return this;
191
191
  }
192
- /**
193
- * Add a regex filter
194
- */
195
- regex(field, pattern) {
196
- this.filters.push({
197
- type: "Condition",
198
- content: {
199
- field,
200
- operator: "Regex",
201
- value: pattern,
202
- },
203
- });
204
- return this;
205
- }
192
+ // Note: regex filtering is pending server-side support. The server has no
193
+ // Regex filter operator; use contains/startsWith/endsWith instead.
206
194
  // ========================================================================
207
195
  // Logical Operators
208
196
  // ========================================================================
@@ -95,10 +95,6 @@ const query_builder_1 = require("./query-builder");
95
95
  const query = new query_builder_1.QueryBuilder().endsWith("filename", ".pdf").build();
96
96
  (0, vitest_1.expect)(query.filter.content.operator).toBe("EndsWith");
97
97
  });
98
- (0, vitest_1.it)("builds regex filter", () => {
99
- const query = new query_builder_1.QueryBuilder().regex("phone", "^\\+1").build();
100
- (0, vitest_1.expect)(query.filter.content.operator).toBe("Regex");
101
- });
102
98
  });
103
99
  // ============================================================================
104
100
  // Logical Operators Tests
@@ -368,7 +364,6 @@ const query_builder_1 = require("./query-builder");
368
364
  (0, vitest_1.expect)(qb.contains("i", "j")).toBe(qb);
369
365
  (0, vitest_1.expect)(qb.startsWith("k", "l")).toBe(qb);
370
366
  (0, vitest_1.expect)(qb.endsWith("m", "n")).toBe(qb);
371
- (0, vitest_1.expect)(qb.regex("o", "p")).toBe(qb);
372
367
  (0, vitest_1.expect)(qb.sortAsc("q")).toBe(qb);
373
368
  (0, vitest_1.expect)(qb.sortDesc("r")).toBe(qb);
374
369
  (0, vitest_1.expect)(qb.limit(1)).toBe(qb);
package/dist/utils.js CHANGED
@@ -33,7 +33,13 @@ exports.extractRecord = extractRecord;
33
33
  * ```
34
34
  */
35
35
  function getValue(field) {
36
- if (field && typeof field === "object" && "value" in field) {
36
+ // Only unwrap a genuine typed wrapper one carrying BOTH a "type"
37
+ // discriminator and a "value". A user object that merely has a "value" key
38
+ // (e.g. { value: 1, currency: "USD" }) must pass through untouched.
39
+ if (field &&
40
+ typeof field === "object" &&
41
+ "type" in field &&
42
+ "value" in field) {
37
43
  return field.value;
38
44
  }
39
45
  return field;
@@ -29,6 +29,10 @@ const utils_1 = require("./utils");
29
29
  const field = { type: "Null", value: null };
30
30
  (0, vitest_1.expect)((0, utils_1.getValue)(field)).toBeNull();
31
31
  });
32
+ (0, vitest_1.it)("passes through a user object that has a value key but no type (regression #134)", () => {
33
+ const field = { value: 1, currency: "USD" };
34
+ (0, vitest_1.expect)((0, utils_1.getValue)(field)).toEqual(field);
35
+ });
32
36
  (0, vitest_1.it)("returns plain string as-is", () => {
33
37
  (0, vitest_1.expect)((0, utils_1.getValue)("plain string")).toBe("plain string");
34
38
  });
@@ -91,6 +91,31 @@ function waitForMessage(ws) {
91
91
  });
92
92
  });
93
93
  // --------------------------------------------------------------------------
94
+ // URL normalization
95
+ // --------------------------------------------------------------------------
96
+ (0, vitest_1.describe)("URL normalization", () => {
97
+ function captureRequestPath() {
98
+ return new Promise((resolve) => {
99
+ wss.once("connection", (_ws, req) => resolve(req.url ?? ""));
100
+ });
101
+ }
102
+ (0, vitest_1.it)("appends /api/ws without a double slash when the base URL has a trailing slash", async () => {
103
+ const pathPromise = captureRequestPath();
104
+ const client = new client_1.WebSocketClient(`ws://localhost:${port}/`, "test-token");
105
+ // Trigger a connect; we only assert the path the server receives.
106
+ client.findAll("users").catch(() => { });
107
+ (0, vitest_1.expect)(await pathPromise).toBe("/api/ws");
108
+ client.close();
109
+ });
110
+ (0, vitest_1.it)("does not duplicate /api/ws when the URL already ends with it plus a trailing slash", async () => {
111
+ const pathPromise = captureRequestPath();
112
+ const client = new client_1.WebSocketClient(`ws://localhost:${port}/api/ws/`, "test-token");
113
+ client.findAll("users").catch(() => { });
114
+ (0, vitest_1.expect)(await pathPromise).toBe("/api/ws");
115
+ client.close();
116
+ });
117
+ });
118
+ // --------------------------------------------------------------------------
94
119
  // subscribe
95
120
  // --------------------------------------------------------------------------
96
121
  (0, vitest_1.describe)("subscribe", () => {
@@ -493,4 +518,159 @@ function waitForMessage(ws) {
493
518
  client.close();
494
519
  });
495
520
  });
521
+ // --------------------------------------------------------------------------
522
+ // Per-request timeout
523
+ // --------------------------------------------------------------------------
524
+ (0, vitest_1.describe)("per-request timeout", () => {
525
+ (0, vitest_1.it)("rejects a pending request when no response arrives", async () => {
526
+ const client = new client_1.WebSocketClient(`ws://localhost:${port}/api/ws`, "test-token", { requestTimeoutMs: 50, autoReconnect: false });
527
+ const resultPromise = client.findAll("users");
528
+ await new Promise((r) => wss.once("connection", r));
529
+ const ws = getLastConnection();
530
+ await waitForMessage(ws); // receive FindAll but never respond
531
+ await (0, vitest_1.expect)(resultPromise).rejects.toThrow(/timed out after 50ms/);
532
+ client.close();
533
+ });
534
+ (0, vitest_1.it)("does not time out when a response arrives in time", async () => {
535
+ const client = new client_1.WebSocketClient(`ws://localhost:${port}/api/ws`, "test-token", { requestTimeoutMs: 500, autoReconnect: false });
536
+ const resultPromise = client.findAll("users");
537
+ await new Promise((r) => wss.once("connection", r));
538
+ const ws = getLastConnection();
539
+ const msg = await waitForMessage(ws);
540
+ ws.send(JSON.stringify({
541
+ type: "Success",
542
+ payload: { message_id: msg.messageId, data: [{ id: "1" }] },
543
+ }));
544
+ await (0, vitest_1.expect)(resultPromise).resolves.toEqual([{ id: "1" }]);
545
+ client.close();
546
+ });
547
+ });
548
+ // --------------------------------------------------------------------------
549
+ // Reject pending requests on disconnect
550
+ // --------------------------------------------------------------------------
551
+ (0, vitest_1.describe)("disconnect handling", () => {
552
+ (0, vitest_1.it)("rejects in-flight requests when the socket drops", async () => {
553
+ const client = new client_1.WebSocketClient(`ws://localhost:${port}/api/ws`, "test-token", { autoReconnect: false });
554
+ const resultPromise = client.findAll("users");
555
+ await new Promise((r) => wss.once("connection", r));
556
+ const ws = getLastConnection();
557
+ await waitForMessage(ws); // FindAll received, never answered
558
+ // Server drops the connection.
559
+ ws.close();
560
+ await (0, vitest_1.expect)(resultPromise).rejects.toThrow(/connection closed/i);
561
+ client.close();
562
+ });
563
+ });
564
+ // --------------------------------------------------------------------------
565
+ // Backoff helper
566
+ // --------------------------------------------------------------------------
567
+ (0, vitest_1.describe)("computeBackoff", () => {
568
+ (0, vitest_1.it)("grows exponentially, stays within jittered bounds, and caps", () => {
569
+ const client = new client_1.WebSocketClient(`ws://localhost:${port}/api/ws`, "test-token", { reconnectInitialDelayMs: 200, reconnectMaxDelayMs: 5000 });
570
+ // attempt 0 -> base 200, +/-25% => [150, 250]
571
+ for (let i = 0; i < 20; i++) {
572
+ const d0 = client.computeBackoff(0);
573
+ (0, vitest_1.expect)(d0).toBeGreaterThanOrEqual(150);
574
+ (0, vitest_1.expect)(d0).toBeLessThanOrEqual(250);
575
+ }
576
+ // attempt 3 -> base 1600, +/-25% => [1200, 2000]
577
+ const d3 = client.computeBackoff(3);
578
+ (0, vitest_1.expect)(d3).toBeGreaterThanOrEqual(1200);
579
+ (0, vitest_1.expect)(d3).toBeLessThanOrEqual(2000);
580
+ // attempt 20 -> capped at 5000, +/-25% => [3750, 6250]
581
+ const dBig = client.computeBackoff(20);
582
+ (0, vitest_1.expect)(dBig).toBeGreaterThanOrEqual(3750);
583
+ (0, vitest_1.expect)(dBig).toBeLessThanOrEqual(6250);
584
+ client.close();
585
+ });
586
+ });
587
+ // --------------------------------------------------------------------------
588
+ // Auto-reconnect + re-subscribe + fresh token
589
+ // --------------------------------------------------------------------------
590
+ (0, vitest_1.describe)("auto-reconnect", () => {
591
+ (0, vitest_1.it)("re-subscribes after a socket drop and re-evaluates the token", async () => {
592
+ // Token provider returns a new token each call so we can assert the
593
+ // reconnect used a freshly-obtained token (not a stale snapshot).
594
+ let tokenCalls = 0;
595
+ const tokenProvider = () => `token-${++tokenCalls}`;
596
+ const client = new client_1.WebSocketClient(`ws://localhost:${port}/api/ws`, tokenProvider, {
597
+ autoReconnect: true,
598
+ reconnectInitialDelayMs: 10,
599
+ reconnectMaxDelayMs: 30,
600
+ });
601
+ // Establish the subscription on the first connection.
602
+ const streamPromise = client.subscribe("orders");
603
+ await new Promise((r) => wss.once("connection", r));
604
+ const ws1 = getLastConnection();
605
+ const sub1 = await waitForMessage(ws1);
606
+ (0, vitest_1.expect)(sub1.type).toBe("Subscribe");
607
+ (0, vitest_1.expect)(sub1.payload.collection).toBe("orders");
608
+ ws1.send(JSON.stringify({
609
+ type: "Success",
610
+ payload: { message_id: sub1.messageId, status: "subscribed" },
611
+ }));
612
+ const stream = await streamPromise;
613
+ (0, vitest_1.expect)(tokenCalls).toBe(1);
614
+ // Arm the next-connection handler BEFORE dropping so we don't miss it.
615
+ const nextConn = new Promise((resolve) => {
616
+ wss.once("connection", (ws) => resolve(ws));
617
+ });
618
+ // Simulate a transient drop: server closes the socket.
619
+ ws1.close();
620
+ // The client should auto-reconnect (new connection) and re-send Subscribe.
621
+ const ws2 = await nextConn;
622
+ const sub2 = await waitForMessage(ws2);
623
+ (0, vitest_1.expect)(sub2.type).toBe("Subscribe");
624
+ (0, vitest_1.expect)(sub2.payload.collection).toBe("orders");
625
+ // Reconnect re-evaluated the token provider.
626
+ (0, vitest_1.expect)(tokenCalls).toBeGreaterThanOrEqual(2);
627
+ // Ack the re-subscribe.
628
+ ws2.send(JSON.stringify({
629
+ type: "Success",
630
+ payload: { message_id: sub2.messageId, status: "subscribed" },
631
+ }));
632
+ // The SAME stream still delivers mutations over the new socket.
633
+ const mutationPromise = new Promise((resolve) => {
634
+ stream.on("mutation", resolve);
635
+ });
636
+ ws2.send(JSON.stringify({
637
+ type: "MutationNotification",
638
+ payload: {
639
+ collection: "orders",
640
+ event: "insert",
641
+ record_ids: ["order-9"],
642
+ timestamp: "2026-06-02T00:00:00Z",
643
+ },
644
+ }));
645
+ const mutation = await mutationPromise;
646
+ (0, vitest_1.expect)(mutation.recordIds).toEqual(["order-9"]);
647
+ (0, vitest_1.expect)(stream.closed).toBe(false);
648
+ client.close();
649
+ });
650
+ (0, vitest_1.it)("does not reconnect after an intentional close()", async () => {
651
+ const client = new client_1.WebSocketClient(`ws://localhost:${port}/api/ws`, "test-token", { autoReconnect: true, reconnectInitialDelayMs: 10 });
652
+ const streamPromise = client.subscribe("widgets");
653
+ await new Promise((r) => wss.once("connection", r));
654
+ const ws = getLastConnection();
655
+ const sub = await waitForMessage(ws);
656
+ ws.send(JSON.stringify({
657
+ type: "Success",
658
+ payload: { message_id: sub.messageId, status: "subscribed" },
659
+ }));
660
+ await streamPromise;
661
+ const connectionsBefore = serverConnections.length;
662
+ // Intentional close: must NOT reconnect.
663
+ client.close();
664
+ // Give any erroneous reconnect time to land.
665
+ await new Promise((r) => setTimeout(r, 80));
666
+ (0, vitest_1.expect)(serverConnections.length).toBe(connectionsBefore);
667
+ });
668
+ });
669
+ (0, vitest_1.describe)("auth token validation", () => {
670
+ (0, vitest_1.it)("rejects connect when the token provider returns null (no Bearer null)", async () => {
671
+ const client = new client_1.WebSocketClient(`ws://localhost:${port}/api/ws`, () => null);
672
+ await (0, vitest_1.expect)(client.findAll("users")).rejects.toThrow(/token is unavailable/i);
673
+ client.close();
674
+ });
675
+ });
496
676
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekodb/ekodb-client",
3
- "version": "0.18.2",
3
+ "version": "0.20.0",
4
4
  "description": "Official TypeScript/JavaScript client for ekoDB",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -27,6 +27,6 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@msgpack/msgpack": "^3.1.3",
30
- "ws": "^8.19.0"
30
+ "ws": "^8.20.1"
31
31
  }
32
32
  }