@ekodb/ekodb-client 0.20.0 → 0.22.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 +28 -0
- package/dist/client.d.ts +101 -12
- package/dist/client.js +269 -56
- package/dist/client.test.js +206 -0
- package/dist/search.d.ts +12 -0
- package/dist/search.js +9 -0
- package/dist/websocket.test.js +159 -5
- package/package.json +1 -1
- package/src/client.test.ts +287 -0
- package/src/client.ts +367 -65
- package/src/search.ts +18 -0
- package/src/websocket.test.ts +225 -5
package/dist/client.js
CHANGED
|
@@ -402,8 +402,8 @@ class EkoDBClient {
|
|
|
402
402
|
params.append("transaction_id", options.transactionId);
|
|
403
403
|
}
|
|
404
404
|
const url = params.toString()
|
|
405
|
-
? `/api/insert/${collection}?${params.toString()}`
|
|
406
|
-
: `/api/insert/${collection}`;
|
|
405
|
+
? `/api/insert/${encodeURIComponent(collection)}?${params.toString()}`
|
|
406
|
+
: `/api/insert/${encodeURIComponent(collection)}`;
|
|
407
407
|
return this.makeRequest("POST", url, data);
|
|
408
408
|
}
|
|
409
409
|
/**
|
|
@@ -428,15 +428,57 @@ class EkoDBClient {
|
|
|
428
428
|
* const results = await client.find("users", { limit: 10 });
|
|
429
429
|
* ```
|
|
430
430
|
*/
|
|
431
|
-
async find(collection, query = {}) {
|
|
431
|
+
async find(collection, query = {}, options) {
|
|
432
432
|
const queryObj = query instanceof query_builder_1.QueryBuilder ? query.build() : query;
|
|
433
|
-
|
|
433
|
+
// bypass_ripple and transaction_id are query parameters — the same way every
|
|
434
|
+
// other method (insert/update/findById) carries bypass_ripple — not part of
|
|
435
|
+
// the FindBody. Hoist any bypass_ripple carried on the query object (e.g. from
|
|
436
|
+
// QueryBuilder.bypassRipple()) out of the body so it is ALWAYS sent as a query
|
|
437
|
+
// param; an explicit options.bypassRipple wins.
|
|
438
|
+
let body = queryObj;
|
|
439
|
+
let bypassRipple = options?.bypassRipple;
|
|
440
|
+
if (body && typeof body === "object" && "bypass_ripple" in body) {
|
|
441
|
+
const { bypass_ripple, ...rest } = body;
|
|
442
|
+
body = rest;
|
|
443
|
+
if (bypassRipple === undefined)
|
|
444
|
+
bypassRipple = bypass_ripple;
|
|
445
|
+
}
|
|
446
|
+
const params = new URLSearchParams();
|
|
447
|
+
if (options?.transactionId)
|
|
448
|
+
params.append("transaction_id", options.transactionId);
|
|
449
|
+
if (bypassRipple !== undefined)
|
|
450
|
+
params.append("bypass_ripple", String(bypassRipple));
|
|
451
|
+
const qs = params.toString();
|
|
452
|
+
const url = qs
|
|
453
|
+
? `/api/find/${encodeURIComponent(collection)}?${qs}`
|
|
454
|
+
: `/api/find/${encodeURIComponent(collection)}`;
|
|
455
|
+
return this.makeRequest("POST", url, body);
|
|
434
456
|
}
|
|
435
457
|
/**
|
|
436
|
-
* Find a document by ID
|
|
458
|
+
* Find a document by ID.
|
|
459
|
+
* @param options - Optional read options. `transactionId` reads within a
|
|
460
|
+
* transaction (read-your-writes); see {@link FindByIdOptions}.
|
|
437
461
|
*/
|
|
438
|
-
async findById(collection, id) {
|
|
439
|
-
|
|
462
|
+
async findById(collection, id, options) {
|
|
463
|
+
const params = new URLSearchParams();
|
|
464
|
+
if (options?.selectFields?.length) {
|
|
465
|
+
params.append("select_fields", options.selectFields.join(","));
|
|
466
|
+
}
|
|
467
|
+
if (options?.excludeFields?.length) {
|
|
468
|
+
params.append("exclude_fields", options.excludeFields.join(","));
|
|
469
|
+
}
|
|
470
|
+
// bypass_ripple is a GET query param, the same way the non-transactional
|
|
471
|
+
// findById carries it; it rides alongside transaction_id when both are set.
|
|
472
|
+
if (options?.bypassRipple !== undefined) {
|
|
473
|
+
params.append("bypass_ripple", String(options.bypassRipple));
|
|
474
|
+
}
|
|
475
|
+
if (options?.transactionId) {
|
|
476
|
+
params.append("transaction_id", options.transactionId);
|
|
477
|
+
}
|
|
478
|
+
const url = params.toString()
|
|
479
|
+
? `/api/find/${encodeURIComponent(collection)}/${encodeURIComponent(id)}?${params.toString()}`
|
|
480
|
+
: `/api/find/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`;
|
|
481
|
+
return this.makeRequest("GET", url);
|
|
440
482
|
}
|
|
441
483
|
/**
|
|
442
484
|
* Find a document by ID with field projection
|
|
@@ -444,8 +486,9 @@ class EkoDBClient {
|
|
|
444
486
|
* @param id - Document ID
|
|
445
487
|
* @param selectFields - Fields to include in the result
|
|
446
488
|
* @param excludeFields - Fields to exclude from the result
|
|
489
|
+
* @param transactionId - Read within a transaction (read-your-writes)
|
|
447
490
|
*/
|
|
448
|
-
async findByIdWithProjection(collection, id, selectFields, excludeFields) {
|
|
491
|
+
async findByIdWithProjection(collection, id, selectFields, excludeFields, transactionId) {
|
|
449
492
|
const params = new URLSearchParams();
|
|
450
493
|
if (selectFields?.length) {
|
|
451
494
|
params.append("select_fields", selectFields.join(","));
|
|
@@ -453,9 +496,12 @@ class EkoDBClient {
|
|
|
453
496
|
if (excludeFields?.length) {
|
|
454
497
|
params.append("exclude_fields", excludeFields.join(","));
|
|
455
498
|
}
|
|
499
|
+
if (transactionId) {
|
|
500
|
+
params.append("transaction_id", transactionId);
|
|
501
|
+
}
|
|
456
502
|
const url = params.toString()
|
|
457
|
-
? `/api/find/${collection}/${id}?${params.toString()}`
|
|
458
|
-
: `/api/find/${collection}/${id}`;
|
|
503
|
+
? `/api/find/${encodeURIComponent(collection)}/${encodeURIComponent(id)}?${params.toString()}`
|
|
504
|
+
: `/api/find/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`;
|
|
459
505
|
return this.makeRequest("GET", url);
|
|
460
506
|
}
|
|
461
507
|
/**
|
|
@@ -474,8 +520,8 @@ class EkoDBClient {
|
|
|
474
520
|
params.append("transaction_id", options.transactionId);
|
|
475
521
|
}
|
|
476
522
|
const url = params.toString()
|
|
477
|
-
? `/api/update/${collection}/${id}?${params.toString()}`
|
|
478
|
-
: `/api/update/${collection}/${id}`;
|
|
523
|
+
? `/api/update/${encodeURIComponent(collection)}/${encodeURIComponent(id)}?${params.toString()}`
|
|
524
|
+
: `/api/update/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`;
|
|
479
525
|
return this.makeRequest("PUT", url, record);
|
|
480
526
|
}
|
|
481
527
|
/**
|
|
@@ -492,7 +538,7 @@ class EkoDBClient {
|
|
|
492
538
|
* @param value - The value for the action (omit for pop/shift/clear)
|
|
493
539
|
*/
|
|
494
540
|
async updateWithAction(collection, id, action, field, value) {
|
|
495
|
-
const url = `/api/update/${collection}/${id}/action/${action}`;
|
|
541
|
+
const url = `/api/update/${encodeURIComponent(collection)}/${encodeURIComponent(id)}/action/${encodeURIComponent(action)}`;
|
|
496
542
|
return this.makeRequest("PUT", url, {
|
|
497
543
|
field,
|
|
498
544
|
value: value ?? null,
|
|
@@ -509,7 +555,7 @@ class EkoDBClient {
|
|
|
509
555
|
* @param actions - Array of [action, field, value] tuples
|
|
510
556
|
*/
|
|
511
557
|
async updateWithActionSequence(collection, id, actions) {
|
|
512
|
-
const url = `/api/update/sequence/${collection}/${id}`;
|
|
558
|
+
const url = `/api/update/sequence/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`;
|
|
513
559
|
return this.makeRequest("PUT", url, actions);
|
|
514
560
|
}
|
|
515
561
|
/**
|
|
@@ -527,8 +573,8 @@ class EkoDBClient {
|
|
|
527
573
|
params.append("transaction_id", options.transactionId);
|
|
528
574
|
}
|
|
529
575
|
const url = params.toString()
|
|
530
|
-
? `/api/delete/${collection}/${id}?${params.toString()}`
|
|
531
|
-
: `/api/delete/${collection}/${id}`;
|
|
576
|
+
? `/api/delete/${encodeURIComponent(collection)}/${encodeURIComponent(id)}?${params.toString()}`
|
|
577
|
+
: `/api/delete/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`;
|
|
532
578
|
await this.makeRequest("DELETE", url);
|
|
533
579
|
}
|
|
534
580
|
/**
|
|
@@ -547,8 +593,8 @@ class EkoDBClient {
|
|
|
547
593
|
}
|
|
548
594
|
const inserts = records.map((data) => ({ data }));
|
|
549
595
|
const url = params.toString()
|
|
550
|
-
? `/api/batch/insert/${collection}?${params.toString()}`
|
|
551
|
-
: `/api/batch/insert/${collection}`;
|
|
596
|
+
? `/api/batch/insert/${encodeURIComponent(collection)}?${params.toString()}`
|
|
597
|
+
: `/api/batch/insert/${encodeURIComponent(collection)}`;
|
|
552
598
|
return this.makeRequest("POST", url, { inserts });
|
|
553
599
|
}
|
|
554
600
|
/**
|
|
@@ -560,7 +606,7 @@ class EkoDBClient {
|
|
|
560
606
|
data: u.data,
|
|
561
607
|
bypass_ripple: u.bypassRipple,
|
|
562
608
|
}));
|
|
563
|
-
return this.makeRequest("PUT", `/api/batch/update/${collection}`, { updates: formattedUpdates });
|
|
609
|
+
return this.makeRequest("PUT", `/api/batch/update/${encodeURIComponent(collection)}`, { updates: formattedUpdates });
|
|
564
610
|
}
|
|
565
611
|
/**
|
|
566
612
|
* Batch delete multiple documents
|
|
@@ -570,7 +616,7 @@ class EkoDBClient {
|
|
|
570
616
|
id: id,
|
|
571
617
|
bypass_ripple: bypassRipple,
|
|
572
618
|
}));
|
|
573
|
-
return this.makeRequest("DELETE", `/api/batch/delete/${collection}`, { deletes });
|
|
619
|
+
return this.makeRequest("DELETE", `/api/batch/delete/${encodeURIComponent(collection)}`, { deletes });
|
|
574
620
|
}
|
|
575
621
|
/**
|
|
576
622
|
* Set a key-value pair with optional TTL
|
|
@@ -598,6 +644,12 @@ class EkoDBClient {
|
|
|
598
644
|
async kvDelete(key) {
|
|
599
645
|
await this.makeRequest("DELETE", `/api/kv/delete/${encodeURIComponent(key)}`, undefined, 0, true);
|
|
600
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* Clear the entire KV store (all keys in the namespace).
|
|
649
|
+
*/
|
|
650
|
+
async kvClear() {
|
|
651
|
+
await this.makeRequest("DELETE", "/api/kv/clear", undefined, 0, true);
|
|
652
|
+
}
|
|
601
653
|
/**
|
|
602
654
|
* Batch get multiple keys
|
|
603
655
|
* @param keys - Array of keys to retrieve
|
|
@@ -663,7 +715,18 @@ class EkoDBClient {
|
|
|
663
715
|
// Transaction Operations
|
|
664
716
|
// ============================================================================
|
|
665
717
|
/**
|
|
666
|
-
* Begin a new transaction
|
|
718
|
+
* Begin a new transaction.
|
|
719
|
+
*
|
|
720
|
+
* Transactions are buffered: statements issued with this `transactionId`
|
|
721
|
+
* (passed via the `transactionId` option on insert/update/delete/find/…) are
|
|
722
|
+
* staged and applied atomically only at {@link commitTransaction}. They are
|
|
723
|
+
* invisible to everyone else until commit, and visible to this transaction's
|
|
724
|
+
* own reads (read-your-writes) only when those reads also carry the
|
|
725
|
+
* `transactionId`. {@link rollbackTransaction} discards the staged writes.
|
|
726
|
+
* `commitTransaction` may reject with a conflict (HTTP 409) if a record this
|
|
727
|
+
* transaction read or wrote was changed by another committed transaction —
|
|
728
|
+
* retry the transaction in that case.
|
|
729
|
+
*
|
|
667
730
|
* @param isolationLevel - Transaction isolation level (default: "ReadCommitted")
|
|
668
731
|
* @returns Transaction ID
|
|
669
732
|
*/
|
|
@@ -687,12 +750,31 @@ class EkoDBClient {
|
|
|
687
750
|
await this.makeRequest("POST", `/api/transactions/${encodeURIComponent(transactionId)}/commit`, undefined, 0, true);
|
|
688
751
|
}
|
|
689
752
|
/**
|
|
690
|
-
* Rollback a transaction
|
|
753
|
+
* Rollback a transaction (discards all staged writes; nothing was applied).
|
|
691
754
|
* @param transactionId - The transaction ID to rollback
|
|
692
755
|
*/
|
|
693
756
|
async rollbackTransaction(transactionId) {
|
|
694
757
|
await this.makeRequest("POST", `/api/transactions/${encodeURIComponent(transactionId)}/rollback`, undefined, 0, true);
|
|
695
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* Create a savepoint within a transaction. A later
|
|
761
|
+
* {@link rollbackToSavepoint} discards everything staged after it.
|
|
762
|
+
*/
|
|
763
|
+
async createSavepoint(transactionId, name) {
|
|
764
|
+
await this.makeRequest("POST", `/api/transactions/${encodeURIComponent(transactionId)}/savepoints`, { name }, 0, true);
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Roll the transaction back to a savepoint, discarding writes staged after it.
|
|
768
|
+
*/
|
|
769
|
+
async rollbackToSavepoint(transactionId, name) {
|
|
770
|
+
await this.makeRequest("POST", `/api/transactions/${encodeURIComponent(transactionId)}/savepoints/${encodeURIComponent(name)}/rollback`, undefined, 0, true);
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Release (forget) a savepoint. Staged work is unaffected.
|
|
774
|
+
*/
|
|
775
|
+
async releaseSavepoint(transactionId, name) {
|
|
776
|
+
await this.makeRequest("DELETE", `/api/transactions/${encodeURIComponent(transactionId)}/savepoints/${encodeURIComponent(name)}`, undefined, 0, true);
|
|
777
|
+
}
|
|
696
778
|
// ============================================================================
|
|
697
779
|
// Convenience Methods
|
|
698
780
|
// ============================================================================
|
|
@@ -833,11 +915,18 @@ class EkoDBClient {
|
|
|
833
915
|
const result = await this.makeRequest("GET", "/api/collections", undefined, 0, true);
|
|
834
916
|
return result.collections;
|
|
835
917
|
}
|
|
918
|
+
/**
|
|
919
|
+
* List collections, excluding internal chat/system collections.
|
|
920
|
+
*/
|
|
921
|
+
async listUserCollections() {
|
|
922
|
+
const result = await this.makeRequest("GET", "/api/collections?exclude_internal=true", undefined, 0, true);
|
|
923
|
+
return result.collections;
|
|
924
|
+
}
|
|
836
925
|
/**
|
|
837
926
|
* Delete a collection
|
|
838
927
|
*/
|
|
839
928
|
async deleteCollection(collection) {
|
|
840
|
-
await this.makeRequest("DELETE", `/api/collections/${collection}`, undefined, 0, true);
|
|
929
|
+
await this.makeRequest("DELETE", `/api/collections/${encodeURIComponent(collection)}`, undefined, 0, true);
|
|
841
930
|
}
|
|
842
931
|
/**
|
|
843
932
|
* Restore a deleted record from trash
|
|
@@ -848,7 +937,7 @@ class EkoDBClient {
|
|
|
848
937
|
* @returns true if restored successfully
|
|
849
938
|
*/
|
|
850
939
|
async restoreRecord(collection, id) {
|
|
851
|
-
const result = await this.makeRequest("POST", `/api/trash/${collection}/${id}`, undefined, 0, true);
|
|
940
|
+
const result = await this.makeRequest("POST", `/api/trash/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`, undefined, 0, true);
|
|
852
941
|
return result.status === "restored";
|
|
853
942
|
}
|
|
854
943
|
/**
|
|
@@ -859,7 +948,7 @@ class EkoDBClient {
|
|
|
859
948
|
* @returns Number of records restored
|
|
860
949
|
*/
|
|
861
950
|
async restoreCollection(collection) {
|
|
862
|
-
const result = await this.makeRequest("POST", `/api/trash/${collection}`, undefined, 0, true);
|
|
951
|
+
const result = await this.makeRequest("POST", `/api/trash/${encodeURIComponent(collection)}`, undefined, 0, true);
|
|
863
952
|
return { recordsRestored: result.records_restored };
|
|
864
953
|
}
|
|
865
954
|
/**
|
|
@@ -880,7 +969,7 @@ class EkoDBClient {
|
|
|
880
969
|
*/
|
|
881
970
|
async createCollection(collection, schema) {
|
|
882
971
|
const schemaObj = schema instanceof schema_1.SchemaBuilder ? schema.build() : schema;
|
|
883
|
-
await this.makeRequest("POST", `/api/collections/${collection}`, schemaObj, 0, true);
|
|
972
|
+
await this.makeRequest("POST", `/api/collections/${encodeURIComponent(collection)}`, schemaObj, 0, true);
|
|
884
973
|
}
|
|
885
974
|
/**
|
|
886
975
|
* Get collection metadata and schema
|
|
@@ -889,7 +978,7 @@ class EkoDBClient {
|
|
|
889
978
|
* @returns Collection metadata including schema and analytics
|
|
890
979
|
*/
|
|
891
980
|
async getCollection(collection) {
|
|
892
|
-
return this.makeRequest("GET", `/api/collections/${collection}`, undefined, 0, true);
|
|
981
|
+
return this.makeRequest("GET", `/api/collections/${encodeURIComponent(collection)}`, undefined, 0, true);
|
|
893
982
|
}
|
|
894
983
|
/**
|
|
895
984
|
* Get collection schema
|
|
@@ -936,7 +1025,7 @@ class EkoDBClient {
|
|
|
936
1025
|
*/
|
|
937
1026
|
async search(collection, query) {
|
|
938
1027
|
// Ensure all parameters from SearchQuery are sent to server
|
|
939
|
-
return this.makeRequest("POST", `/api/search/${collection}`, query, 0, true);
|
|
1028
|
+
return this.makeRequest("POST", `/api/search/${encodeURIComponent(collection)}`, query, 0, true);
|
|
940
1029
|
}
|
|
941
1030
|
/**
|
|
942
1031
|
* Get distinct (unique) values for a field across all records in a collection.
|
|
@@ -966,7 +1055,7 @@ class EkoDBClient {
|
|
|
966
1055
|
body.bypass_ripple = options.bypassRipple;
|
|
967
1056
|
if (options.bypassCache !== undefined)
|
|
968
1057
|
body.bypass_cache = options.bypassCache;
|
|
969
|
-
return this.makeRequest("POST", `/api/distinct/${collection}/${field}`, body, 0, true);
|
|
1058
|
+
return this.makeRequest("POST", `/api/distinct/${encodeURIComponent(collection)}/${encodeURIComponent(field)}`, body, 0, true);
|
|
970
1059
|
}
|
|
971
1060
|
/**
|
|
972
1061
|
* Health check - verify the ekoDB server is responding
|
|
@@ -1146,20 +1235,36 @@ class EkoDBClient {
|
|
|
1146
1235
|
* Send a message in an existing chat session
|
|
1147
1236
|
*/
|
|
1148
1237
|
async chatMessage(sessionId, request) {
|
|
1149
|
-
return this.makeRequest("POST", `/api/chat/${sessionId}/messages`, request, 0, true);
|
|
1238
|
+
return this.makeRequest("POST", `/api/chat/${encodeURIComponent(sessionId)}/messages`, request, 0, true);
|
|
1150
1239
|
}
|
|
1151
1240
|
/**
|
|
1152
1241
|
* Submit a client tool result for an in-flight SSE chat stream.
|
|
1153
1242
|
* Unblocks ekoDB's tool loop so it can feed the result to the LLM.
|
|
1154
1243
|
*/
|
|
1155
1244
|
async submitChatToolResult(chatId, callId, success, result, error) {
|
|
1156
|
-
await this.makeRequest("POST", `/api/chat/${chatId}/tool-result`, {
|
|
1245
|
+
await this.makeRequest("POST", `/api/chat/${encodeURIComponent(chatId)}/tool-result`, {
|
|
1157
1246
|
call_id: callId,
|
|
1158
1247
|
success,
|
|
1159
1248
|
...(result !== undefined && { result }),
|
|
1160
1249
|
...(error !== undefined && { error }),
|
|
1161
1250
|
}, 0, true);
|
|
1162
1251
|
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Send a client tool keepalive (liveness ping) for an in-flight SSE chat stream.
|
|
1254
|
+
*
|
|
1255
|
+
* This is NOT a result: it does not unblock the tool loop or feed anything to the
|
|
1256
|
+
* LLM. It simply resets the server's per-tool wait deadline (governed by
|
|
1257
|
+
* `client_tool_timeout_secs`, default 60s) so that slow user confirmations or
|
|
1258
|
+
* long-running client tools don't get the turn timed out before
|
|
1259
|
+
* {@link submitChatToolResult} arrives. Send it periodically while a client tool
|
|
1260
|
+
* is still working. See ekoDB#530.
|
|
1261
|
+
*/
|
|
1262
|
+
async submitChatToolKeepalive(chatId, callId) {
|
|
1263
|
+
await this.makeRequest("POST", `/api/chat/${encodeURIComponent(chatId)}/tool-result`, {
|
|
1264
|
+
call_id: callId,
|
|
1265
|
+
keepalive: true,
|
|
1266
|
+
}, 0, true);
|
|
1267
|
+
}
|
|
1163
1268
|
/**
|
|
1164
1269
|
* Send a message in an existing chat session via SSE streaming.
|
|
1165
1270
|
*
|
|
@@ -1180,7 +1285,7 @@ class EkoDBClient {
|
|
|
1180
1285
|
await this.refreshToken();
|
|
1181
1286
|
token = await this.getToken();
|
|
1182
1287
|
}
|
|
1183
|
-
const url = `${this.baseURL}/api/chat/${chatId}/messages/stream`;
|
|
1288
|
+
const url = `${this.baseURL}/api/chat/${encodeURIComponent(chatId)}/messages/stream`;
|
|
1184
1289
|
const response = await fetch(url, {
|
|
1185
1290
|
method: "POST",
|
|
1186
1291
|
headers: {
|
|
@@ -1277,7 +1382,7 @@ class EkoDBClient {
|
|
|
1277
1382
|
* Get a chat session by ID
|
|
1278
1383
|
*/
|
|
1279
1384
|
async getChatSession(sessionId) {
|
|
1280
|
-
return this.makeRequest("GET", `/api/chat/${sessionId}`, undefined, 0, true);
|
|
1385
|
+
return this.makeRequest("GET", `/api/chat/${encodeURIComponent(sessionId)}`, undefined, 0, true);
|
|
1281
1386
|
}
|
|
1282
1387
|
/**
|
|
1283
1388
|
* List all chat sessions
|
|
@@ -1307,15 +1412,15 @@ class EkoDBClient {
|
|
|
1307
1412
|
params.append("sort", query.sort);
|
|
1308
1413
|
const queryString = params.toString();
|
|
1309
1414
|
const path = queryString
|
|
1310
|
-
? `/api/chat/${sessionId}/messages?${queryString}`
|
|
1311
|
-
: `/api/chat/${sessionId}/messages`;
|
|
1415
|
+
? `/api/chat/${encodeURIComponent(sessionId)}/messages?${queryString}`
|
|
1416
|
+
: `/api/chat/${encodeURIComponent(sessionId)}/messages`;
|
|
1312
1417
|
return this.makeRequest("GET", path, undefined, 0, true);
|
|
1313
1418
|
}
|
|
1314
1419
|
/**
|
|
1315
1420
|
* Update a chat session
|
|
1316
1421
|
*/
|
|
1317
1422
|
async updateChatSession(sessionId, request) {
|
|
1318
|
-
return this.makeRequest("PUT", `/api/chat/${sessionId}`, request, 0, true);
|
|
1423
|
+
return this.makeRequest("PUT", `/api/chat/${encodeURIComponent(sessionId)}`, request, 0, true);
|
|
1319
1424
|
}
|
|
1320
1425
|
/**
|
|
1321
1426
|
* Branch a chat session
|
|
@@ -1327,31 +1432,31 @@ class EkoDBClient {
|
|
|
1327
1432
|
* Delete a chat session
|
|
1328
1433
|
*/
|
|
1329
1434
|
async deleteChatSession(sessionId) {
|
|
1330
|
-
await this.makeRequest("DELETE", `/api/chat/${sessionId}`, undefined, 0, true);
|
|
1435
|
+
await this.makeRequest("DELETE", `/api/chat/${encodeURIComponent(sessionId)}`, undefined, 0, true);
|
|
1331
1436
|
}
|
|
1332
1437
|
/**
|
|
1333
1438
|
* Regenerate an AI response message
|
|
1334
1439
|
*/
|
|
1335
1440
|
async regenerateMessage(sessionId, messageId) {
|
|
1336
|
-
return this.makeRequest("POST", `/api/chat/${sessionId}/messages/${messageId}/regenerate`, undefined, 0, true);
|
|
1441
|
+
return this.makeRequest("POST", `/api/chat/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}/regenerate`, undefined, 0, true);
|
|
1337
1442
|
}
|
|
1338
1443
|
/**
|
|
1339
1444
|
* Update a specific message
|
|
1340
1445
|
*/
|
|
1341
1446
|
async updateChatMessage(sessionId, messageId, content) {
|
|
1342
|
-
await this.makeRequest("PUT", `/api/chat/${sessionId}/messages/${messageId}`, { content }, 0, true);
|
|
1447
|
+
await this.makeRequest("PUT", `/api/chat/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}`, { content }, 0, true);
|
|
1343
1448
|
}
|
|
1344
1449
|
/**
|
|
1345
1450
|
* Delete a specific message
|
|
1346
1451
|
*/
|
|
1347
1452
|
async deleteChatMessage(sessionId, messageId) {
|
|
1348
|
-
await this.makeRequest("DELETE", `/api/chat/${sessionId}/messages/${messageId}`, undefined, 0, true);
|
|
1453
|
+
await this.makeRequest("DELETE", `/api/chat/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}`, undefined, 0, true);
|
|
1349
1454
|
}
|
|
1350
1455
|
/**
|
|
1351
1456
|
* Toggle the "forgotten" status of a message
|
|
1352
1457
|
*/
|
|
1353
1458
|
async toggleForgottenMessage(sessionId, messageId, forgotten) {
|
|
1354
|
-
await this.makeRequest("PATCH", `/api/chat/${sessionId}/messages/${messageId}/forgotten`, { forgotten }, 0, true);
|
|
1459
|
+
await this.makeRequest("PATCH", `/api/chat/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}/forgotten`, { forgotten }, 0, true);
|
|
1355
1460
|
}
|
|
1356
1461
|
/**
|
|
1357
1462
|
* Compact a chat session's history on demand.
|
|
@@ -1368,7 +1473,7 @@ class EkoDBClient {
|
|
|
1368
1473
|
if (keepRecent !== undefined) {
|
|
1369
1474
|
body.keep_recent = keepRecent;
|
|
1370
1475
|
}
|
|
1371
|
-
return this.makeRequest("POST", `/api/chat/${chatId}/compact`, body, 0, true);
|
|
1476
|
+
return this.makeRequest("POST", `/api/chat/${encodeURIComponent(chatId)}/compact`, body, 0, true);
|
|
1372
1477
|
}
|
|
1373
1478
|
/**
|
|
1374
1479
|
* Merge multiple chat sessions into one
|
|
@@ -1406,7 +1511,7 @@ class EkoDBClient {
|
|
|
1406
1511
|
* @returns The chat message record
|
|
1407
1512
|
*/
|
|
1408
1513
|
async getChatMessage(sessionId, messageId) {
|
|
1409
|
-
return this.makeRequest("GET", `/api/chat/${sessionId}/messages/${messageId}`, undefined, 0, true);
|
|
1514
|
+
return this.makeRequest("GET", `/api/chat/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}`, undefined, 0, true);
|
|
1410
1515
|
}
|
|
1411
1516
|
// ========================================================================
|
|
1412
1517
|
// SCRIPTS API
|
|
@@ -1422,7 +1527,7 @@ class EkoDBClient {
|
|
|
1422
1527
|
* Get a function by ID
|
|
1423
1528
|
*/
|
|
1424
1529
|
async getFunction(id) {
|
|
1425
|
-
return this.makeRequest("GET", `/api/functions/${id}`);
|
|
1530
|
+
return this.makeRequest("GET", `/api/functions/${encodeURIComponent(id)}`);
|
|
1426
1531
|
}
|
|
1427
1532
|
/**
|
|
1428
1533
|
* List all functions, optionally filtered by tags
|
|
@@ -1435,19 +1540,19 @@ class EkoDBClient {
|
|
|
1435
1540
|
* Update an existing function by ID
|
|
1436
1541
|
*/
|
|
1437
1542
|
async updateFunction(id, script) {
|
|
1438
|
-
await this.makeRequest("PUT", `/api/functions/${id}`, script);
|
|
1543
|
+
await this.makeRequest("PUT", `/api/functions/${encodeURIComponent(id)}`, script);
|
|
1439
1544
|
}
|
|
1440
1545
|
/**
|
|
1441
1546
|
* Delete a function by ID
|
|
1442
1547
|
*/
|
|
1443
1548
|
async deleteFunction(id) {
|
|
1444
|
-
await this.makeRequest("DELETE", `/api/functions/${id}`);
|
|
1549
|
+
await this.makeRequest("DELETE", `/api/functions/${encodeURIComponent(id)}`);
|
|
1445
1550
|
}
|
|
1446
1551
|
/**
|
|
1447
1552
|
* Call a saved function by ID or label
|
|
1448
1553
|
*/
|
|
1449
1554
|
async callFunction(idOrLabel, params) {
|
|
1450
|
-
return this.makeRequest("POST", `/api/functions/${idOrLabel}`, params || {});
|
|
1555
|
+
return this.makeRequest("POST", `/api/functions/${encodeURIComponent(idOrLabel)}`, params || {});
|
|
1451
1556
|
}
|
|
1452
1557
|
// ========================================================================
|
|
1453
1558
|
// USER FUNCTIONS API
|
|
@@ -1556,15 +1661,15 @@ class EkoDBClient {
|
|
|
1556
1661
|
}
|
|
1557
1662
|
/** Start a goal step (status -> in_progress) */
|
|
1558
1663
|
async goalStepStart(id, stepIndex) {
|
|
1559
|
-
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/start`, undefined, 0, true);
|
|
1664
|
+
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${encodeURIComponent(String(stepIndex))}/start`, undefined, 0, true);
|
|
1560
1665
|
}
|
|
1561
1666
|
/** Complete a goal step with result */
|
|
1562
1667
|
async goalStepComplete(id, stepIndex, data) {
|
|
1563
|
-
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/complete`, data, 0, true);
|
|
1668
|
+
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${encodeURIComponent(String(stepIndex))}/complete`, data, 0, true);
|
|
1564
1669
|
}
|
|
1565
1670
|
/** Fail a goal step with error */
|
|
1566
1671
|
async goalStepFail(id, stepIndex, data) {
|
|
1567
|
-
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/fail`, data, 0, true);
|
|
1672
|
+
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${encodeURIComponent(String(stepIndex))}/fail`, data, 0, true);
|
|
1568
1673
|
}
|
|
1569
1674
|
// ========================================================================
|
|
1570
1675
|
// TASK API
|
|
@@ -2099,6 +2204,13 @@ class WebSocketClient {
|
|
|
2099
2204
|
this.ws = null;
|
|
2100
2205
|
this.dispatcherRunning = false;
|
|
2101
2206
|
this.schemaCache = null;
|
|
2207
|
+
/**
|
|
2208
|
+
* Per-connection wire format, set by negotiateFormat() on every (re)connect:
|
|
2209
|
+
* true once the server has Welcomed msgpack, so frames are sent/received as
|
|
2210
|
+
* binary msgpack; false (JSON text) otherwise, including against an older
|
|
2211
|
+
* server that never Welcomes. Keeps the transport fully back-compatible.
|
|
2212
|
+
*/
|
|
2213
|
+
this.binary = false;
|
|
2102
2214
|
// Reconnect state
|
|
2103
2215
|
/** Set while close() is in progress so the close handler doesn't reconnect. */
|
|
2104
2216
|
this.closed = false;
|
|
@@ -2180,8 +2292,66 @@ class WebSocketClient {
|
|
|
2180
2292
|
this.ws.on("open", () => resolve());
|
|
2181
2293
|
this.ws.on("error", (err) => reject(err));
|
|
2182
2294
|
});
|
|
2295
|
+
// Negotiate the wire format before the dispatcher starts so the Welcome is
|
|
2296
|
+
// consumed here (not by routeMessage), and before any real frame is sent
|
|
2297
|
+
// (resubscribeAll runs only after this resolves).
|
|
2298
|
+
await this.negotiateFormat(this.ws);
|
|
2183
2299
|
this.spawnDispatcher();
|
|
2184
2300
|
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Additive capability handshake: offer msgpack and, if the server Welcomes
|
|
2303
|
+
* it, switch this connection to binary msgpack frames; otherwise stay on JSON
|
|
2304
|
+
* text. The Welcome (a text frame) is read with a one-shot listener and a
|
|
2305
|
+
* timeout so an older server that never answers — or answers with an Error —
|
|
2306
|
+
* simply leaves the connection on JSON. Best-effort and never throws: JSON
|
|
2307
|
+
* always works.
|
|
2308
|
+
*/
|
|
2309
|
+
async negotiateFormat(socket) {
|
|
2310
|
+
this.binary = false;
|
|
2311
|
+
const welcome = await new Promise((resolve) => {
|
|
2312
|
+
const onMsg = (data) => {
|
|
2313
|
+
clearTimeout(timer);
|
|
2314
|
+
try {
|
|
2315
|
+
resolve(JSON.parse(data.toString()));
|
|
2316
|
+
}
|
|
2317
|
+
catch {
|
|
2318
|
+
resolve(null);
|
|
2319
|
+
}
|
|
2320
|
+
};
|
|
2321
|
+
// Only caps the wait when no Welcome comes (a silent/old server); the
|
|
2322
|
+
// listener resolves immediately when it does arrive. 2s comfortably exceeds
|
|
2323
|
+
// the handshake round-trip even on high-latency links.
|
|
2324
|
+
const timer = setTimeout(() => {
|
|
2325
|
+
socket.off("message", onMsg);
|
|
2326
|
+
resolve(null);
|
|
2327
|
+
}, 2000);
|
|
2328
|
+
socket.once("message", onMsg);
|
|
2329
|
+
try {
|
|
2330
|
+
socket.send(JSON.stringify({
|
|
2331
|
+
type: "Hello",
|
|
2332
|
+
payload: { formats: ["msgpack", "json"] },
|
|
2333
|
+
}));
|
|
2334
|
+
}
|
|
2335
|
+
catch {
|
|
2336
|
+
clearTimeout(timer);
|
|
2337
|
+
socket.off("message", onMsg);
|
|
2338
|
+
resolve(null);
|
|
2339
|
+
}
|
|
2340
|
+
});
|
|
2341
|
+
if (welcome &&
|
|
2342
|
+
welcome.type === "Welcome" &&
|
|
2343
|
+
welcome.payload?.format === "msgpack") {
|
|
2344
|
+
this.binary = true;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
/**
|
|
2348
|
+
* Send a request object on the active socket using the negotiated format:
|
|
2349
|
+
* binary msgpack when the server Welcomed it, JSON text otherwise. The single
|
|
2350
|
+
* write point so every request honors the negotiated transport.
|
|
2351
|
+
*/
|
|
2352
|
+
sendFrame(obj) {
|
|
2353
|
+
this.ws.send(this.binary ? (0, msgpack_1.encode)(obj) : JSON.stringify(obj));
|
|
2354
|
+
}
|
|
2185
2355
|
spawnDispatcher() {
|
|
2186
2356
|
if (this.dispatcherRunning)
|
|
2187
2357
|
return;
|
|
@@ -2190,11 +2360,16 @@ class WebSocketClient {
|
|
|
2190
2360
|
// socket may still emit late close/error events; ignore them so they don't
|
|
2191
2361
|
// tear down the replacement connection.
|
|
2192
2362
|
const socket = this.ws;
|
|
2193
|
-
socket.on("message", (data) => {
|
|
2363
|
+
socket.on("message", (data, isBinary) => {
|
|
2194
2364
|
if (this.ws !== socket)
|
|
2195
2365
|
return;
|
|
2196
2366
|
try {
|
|
2197
|
-
|
|
2367
|
+
// A binary frame is msgpack (the server only sends binary once it has
|
|
2368
|
+
// Welcomed msgpack); a text frame is JSON. Decode by frame type so the
|
|
2369
|
+
// routed value is identical regardless of negotiated transport.
|
|
2370
|
+
const msg = isBinary
|
|
2371
|
+
? (0, msgpack_1.decode)(data)
|
|
2372
|
+
: JSON.parse(data.toString());
|
|
2198
2373
|
this.routeMessage(msg);
|
|
2199
2374
|
}
|
|
2200
2375
|
catch {
|
|
@@ -2477,7 +2652,7 @@ class WebSocketClient {
|
|
|
2477
2652
|
}
|
|
2478
2653
|
this.pendingRequests.set(messageId, { resolve, reject, timer });
|
|
2479
2654
|
try {
|
|
2480
|
-
this.
|
|
2655
|
+
this.sendFrame(request);
|
|
2481
2656
|
}
|
|
2482
2657
|
catch (err) {
|
|
2483
2658
|
this.pendingRequests.delete(messageId);
|
|
@@ -2560,6 +2735,27 @@ class WebSocketClient {
|
|
|
2560
2735
|
if (stream && !stream.closed) {
|
|
2561
2736
|
stream.close();
|
|
2562
2737
|
}
|
|
2738
|
+
// Best-effort: tell the server to stop streaming this collection (the
|
|
2739
|
+
// server already handles an Unsubscribe frame). If the socket isn't open
|
|
2740
|
+
// the local teardown above suffices, since the server drops subscriptions
|
|
2741
|
+
// when the connection closes. A unique messageId is attached so the
|
|
2742
|
+
// server's Success ack carries a correlation id: it has no pending request
|
|
2743
|
+
// to match, so it is simply ignored — and because the id is present, the
|
|
2744
|
+
// single-pending fallback can't misroute it to an unrelated request.
|
|
2745
|
+
if (this.ws && this.ws.readyState === 1 /* WebSocket.OPEN */) {
|
|
2746
|
+
try {
|
|
2747
|
+
this.sendFrame({
|
|
2748
|
+
type: "Unsubscribe",
|
|
2749
|
+
messageId: this.genMessageId(),
|
|
2750
|
+
payload: { collection },
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
catch {
|
|
2754
|
+
// Best-effort: the socket can close between the readyState check and the
|
|
2755
|
+
// send. Local teardown already happened, so swallow the failure rather
|
|
2756
|
+
// than throw out of a void teardown call.
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2563
2759
|
}
|
|
2564
2760
|
/**
|
|
2565
2761
|
* Send a chat message and receive a streaming response.
|
|
@@ -2588,7 +2784,7 @@ class WebSocketClient {
|
|
|
2588
2784
|
...(options?.excludeTools && { exclude_tools: options.excludeTools }),
|
|
2589
2785
|
},
|
|
2590
2786
|
};
|
|
2591
|
-
this.
|
|
2787
|
+
this.sendFrame(request);
|
|
2592
2788
|
return stream;
|
|
2593
2789
|
}
|
|
2594
2790
|
/**
|
|
@@ -2608,7 +2804,7 @@ class WebSocketClient {
|
|
|
2608
2804
|
resolve: () => resolve(),
|
|
2609
2805
|
reject: (err) => reject(err),
|
|
2610
2806
|
};
|
|
2611
|
-
this.
|
|
2807
|
+
this.sendFrame(request);
|
|
2612
2808
|
});
|
|
2613
2809
|
}
|
|
2614
2810
|
/**
|
|
@@ -2626,7 +2822,24 @@ class WebSocketClient {
|
|
|
2626
2822
|
...(error !== undefined && { error }),
|
|
2627
2823
|
},
|
|
2628
2824
|
};
|
|
2629
|
-
this.
|
|
2825
|
+
this.sendFrame(request);
|
|
2826
|
+
}
|
|
2827
|
+
/**
|
|
2828
|
+
* Cancel an in-flight streaming chat. Fire-and-forget: tells the server to
|
|
2829
|
+
* stop generating tokens for the given chat.
|
|
2830
|
+
*/
|
|
2831
|
+
async cancelChat(chatId) {
|
|
2832
|
+
await this.ensureConnected();
|
|
2833
|
+
// Attach a unique messageId (same generator as unsubscribe). Any Success ack
|
|
2834
|
+
// from the server then carries a correlation id: it has no pending request to
|
|
2835
|
+
// match, so it is ignored — and because the id is present, the dispatcher's
|
|
2836
|
+
// single-pending fallback can't misroute the ack to an unrelated request.
|
|
2837
|
+
const request = {
|
|
2838
|
+
type: "CancelChat",
|
|
2839
|
+
messageId: this.genMessageId(),
|
|
2840
|
+
payload: { chat_id: chatId },
|
|
2841
|
+
};
|
|
2842
|
+
this.sendFrame(request);
|
|
2630
2843
|
}
|
|
2631
2844
|
/**
|
|
2632
2845
|
* Stateless raw LLM completion via WebSocket.
|