@ekodb/ekodb-client 0.20.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 +28 -0
- package/dist/client.d.ts +90 -12
- package/dist/client.js +253 -56
- package/dist/client.test.js +170 -0
- package/dist/websocket.test.js +159 -5
- package/package.json +1 -1
- package/src/client.test.ts +244 -0
- package/src/client.ts +344 -65
- 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,14 +1235,14 @@ 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 }),
|
|
@@ -1180,7 +1269,7 @@ class EkoDBClient {
|
|
|
1180
1269
|
await this.refreshToken();
|
|
1181
1270
|
token = await this.getToken();
|
|
1182
1271
|
}
|
|
1183
|
-
const url = `${this.baseURL}/api/chat/${chatId}/messages/stream`;
|
|
1272
|
+
const url = `${this.baseURL}/api/chat/${encodeURIComponent(chatId)}/messages/stream`;
|
|
1184
1273
|
const response = await fetch(url, {
|
|
1185
1274
|
method: "POST",
|
|
1186
1275
|
headers: {
|
|
@@ -1277,7 +1366,7 @@ class EkoDBClient {
|
|
|
1277
1366
|
* Get a chat session by ID
|
|
1278
1367
|
*/
|
|
1279
1368
|
async getChatSession(sessionId) {
|
|
1280
|
-
return this.makeRequest("GET", `/api/chat/${sessionId}`, undefined, 0, true);
|
|
1369
|
+
return this.makeRequest("GET", `/api/chat/${encodeURIComponent(sessionId)}`, undefined, 0, true);
|
|
1281
1370
|
}
|
|
1282
1371
|
/**
|
|
1283
1372
|
* List all chat sessions
|
|
@@ -1307,15 +1396,15 @@ class EkoDBClient {
|
|
|
1307
1396
|
params.append("sort", query.sort);
|
|
1308
1397
|
const queryString = params.toString();
|
|
1309
1398
|
const path = queryString
|
|
1310
|
-
? `/api/chat/${sessionId}/messages?${queryString}`
|
|
1311
|
-
: `/api/chat/${sessionId}/messages`;
|
|
1399
|
+
? `/api/chat/${encodeURIComponent(sessionId)}/messages?${queryString}`
|
|
1400
|
+
: `/api/chat/${encodeURIComponent(sessionId)}/messages`;
|
|
1312
1401
|
return this.makeRequest("GET", path, undefined, 0, true);
|
|
1313
1402
|
}
|
|
1314
1403
|
/**
|
|
1315
1404
|
* Update a chat session
|
|
1316
1405
|
*/
|
|
1317
1406
|
async updateChatSession(sessionId, request) {
|
|
1318
|
-
return this.makeRequest("PUT", `/api/chat/${sessionId}`, request, 0, true);
|
|
1407
|
+
return this.makeRequest("PUT", `/api/chat/${encodeURIComponent(sessionId)}`, request, 0, true);
|
|
1319
1408
|
}
|
|
1320
1409
|
/**
|
|
1321
1410
|
* Branch a chat session
|
|
@@ -1327,31 +1416,31 @@ class EkoDBClient {
|
|
|
1327
1416
|
* Delete a chat session
|
|
1328
1417
|
*/
|
|
1329
1418
|
async deleteChatSession(sessionId) {
|
|
1330
|
-
await this.makeRequest("DELETE", `/api/chat/${sessionId}`, undefined, 0, true);
|
|
1419
|
+
await this.makeRequest("DELETE", `/api/chat/${encodeURIComponent(sessionId)}`, undefined, 0, true);
|
|
1331
1420
|
}
|
|
1332
1421
|
/**
|
|
1333
1422
|
* Regenerate an AI response message
|
|
1334
1423
|
*/
|
|
1335
1424
|
async regenerateMessage(sessionId, messageId) {
|
|
1336
|
-
return this.makeRequest("POST", `/api/chat/${sessionId}/messages/${messageId}/regenerate`, undefined, 0, true);
|
|
1425
|
+
return this.makeRequest("POST", `/api/chat/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}/regenerate`, undefined, 0, true);
|
|
1337
1426
|
}
|
|
1338
1427
|
/**
|
|
1339
1428
|
* Update a specific message
|
|
1340
1429
|
*/
|
|
1341
1430
|
async updateChatMessage(sessionId, messageId, content) {
|
|
1342
|
-
await this.makeRequest("PUT", `/api/chat/${sessionId}/messages/${messageId}`, { content }, 0, true);
|
|
1431
|
+
await this.makeRequest("PUT", `/api/chat/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}`, { content }, 0, true);
|
|
1343
1432
|
}
|
|
1344
1433
|
/**
|
|
1345
1434
|
* Delete a specific message
|
|
1346
1435
|
*/
|
|
1347
1436
|
async deleteChatMessage(sessionId, messageId) {
|
|
1348
|
-
await this.makeRequest("DELETE", `/api/chat/${sessionId}/messages/${messageId}`, undefined, 0, true);
|
|
1437
|
+
await this.makeRequest("DELETE", `/api/chat/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}`, undefined, 0, true);
|
|
1349
1438
|
}
|
|
1350
1439
|
/**
|
|
1351
1440
|
* Toggle the "forgotten" status of a message
|
|
1352
1441
|
*/
|
|
1353
1442
|
async toggleForgottenMessage(sessionId, messageId, forgotten) {
|
|
1354
|
-
await this.makeRequest("PATCH", `/api/chat/${sessionId}/messages/${messageId}/forgotten`, { forgotten }, 0, true);
|
|
1443
|
+
await this.makeRequest("PATCH", `/api/chat/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}/forgotten`, { forgotten }, 0, true);
|
|
1355
1444
|
}
|
|
1356
1445
|
/**
|
|
1357
1446
|
* Compact a chat session's history on demand.
|
|
@@ -1368,7 +1457,7 @@ class EkoDBClient {
|
|
|
1368
1457
|
if (keepRecent !== undefined) {
|
|
1369
1458
|
body.keep_recent = keepRecent;
|
|
1370
1459
|
}
|
|
1371
|
-
return this.makeRequest("POST", `/api/chat/${chatId}/compact`, body, 0, true);
|
|
1460
|
+
return this.makeRequest("POST", `/api/chat/${encodeURIComponent(chatId)}/compact`, body, 0, true);
|
|
1372
1461
|
}
|
|
1373
1462
|
/**
|
|
1374
1463
|
* Merge multiple chat sessions into one
|
|
@@ -1406,7 +1495,7 @@ class EkoDBClient {
|
|
|
1406
1495
|
* @returns The chat message record
|
|
1407
1496
|
*/
|
|
1408
1497
|
async getChatMessage(sessionId, messageId) {
|
|
1409
|
-
return this.makeRequest("GET", `/api/chat/${sessionId}/messages/${messageId}`, undefined, 0, true);
|
|
1498
|
+
return this.makeRequest("GET", `/api/chat/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}`, undefined, 0, true);
|
|
1410
1499
|
}
|
|
1411
1500
|
// ========================================================================
|
|
1412
1501
|
// SCRIPTS API
|
|
@@ -1422,7 +1511,7 @@ class EkoDBClient {
|
|
|
1422
1511
|
* Get a function by ID
|
|
1423
1512
|
*/
|
|
1424
1513
|
async getFunction(id) {
|
|
1425
|
-
return this.makeRequest("GET", `/api/functions/${id}`);
|
|
1514
|
+
return this.makeRequest("GET", `/api/functions/${encodeURIComponent(id)}`);
|
|
1426
1515
|
}
|
|
1427
1516
|
/**
|
|
1428
1517
|
* List all functions, optionally filtered by tags
|
|
@@ -1435,19 +1524,19 @@ class EkoDBClient {
|
|
|
1435
1524
|
* Update an existing function by ID
|
|
1436
1525
|
*/
|
|
1437
1526
|
async updateFunction(id, script) {
|
|
1438
|
-
await this.makeRequest("PUT", `/api/functions/${id}`, script);
|
|
1527
|
+
await this.makeRequest("PUT", `/api/functions/${encodeURIComponent(id)}`, script);
|
|
1439
1528
|
}
|
|
1440
1529
|
/**
|
|
1441
1530
|
* Delete a function by ID
|
|
1442
1531
|
*/
|
|
1443
1532
|
async deleteFunction(id) {
|
|
1444
|
-
await this.makeRequest("DELETE", `/api/functions/${id}`);
|
|
1533
|
+
await this.makeRequest("DELETE", `/api/functions/${encodeURIComponent(id)}`);
|
|
1445
1534
|
}
|
|
1446
1535
|
/**
|
|
1447
1536
|
* Call a saved function by ID or label
|
|
1448
1537
|
*/
|
|
1449
1538
|
async callFunction(idOrLabel, params) {
|
|
1450
|
-
return this.makeRequest("POST", `/api/functions/${idOrLabel}`, params || {});
|
|
1539
|
+
return this.makeRequest("POST", `/api/functions/${encodeURIComponent(idOrLabel)}`, params || {});
|
|
1451
1540
|
}
|
|
1452
1541
|
// ========================================================================
|
|
1453
1542
|
// USER FUNCTIONS API
|
|
@@ -1556,15 +1645,15 @@ class EkoDBClient {
|
|
|
1556
1645
|
}
|
|
1557
1646
|
/** Start a goal step (status -> in_progress) */
|
|
1558
1647
|
async goalStepStart(id, stepIndex) {
|
|
1559
|
-
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/start`, undefined, 0, true);
|
|
1648
|
+
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${encodeURIComponent(String(stepIndex))}/start`, undefined, 0, true);
|
|
1560
1649
|
}
|
|
1561
1650
|
/** Complete a goal step with result */
|
|
1562
1651
|
async goalStepComplete(id, stepIndex, data) {
|
|
1563
|
-
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/complete`, data, 0, true);
|
|
1652
|
+
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${encodeURIComponent(String(stepIndex))}/complete`, data, 0, true);
|
|
1564
1653
|
}
|
|
1565
1654
|
/** Fail a goal step with error */
|
|
1566
1655
|
async goalStepFail(id, stepIndex, data) {
|
|
1567
|
-
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/fail`, data, 0, true);
|
|
1656
|
+
return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${encodeURIComponent(String(stepIndex))}/fail`, data, 0, true);
|
|
1568
1657
|
}
|
|
1569
1658
|
// ========================================================================
|
|
1570
1659
|
// TASK API
|
|
@@ -2099,6 +2188,13 @@ class WebSocketClient {
|
|
|
2099
2188
|
this.ws = null;
|
|
2100
2189
|
this.dispatcherRunning = false;
|
|
2101
2190
|
this.schemaCache = null;
|
|
2191
|
+
/**
|
|
2192
|
+
* Per-connection wire format, set by negotiateFormat() on every (re)connect:
|
|
2193
|
+
* true once the server has Welcomed msgpack, so frames are sent/received as
|
|
2194
|
+
* binary msgpack; false (JSON text) otherwise, including against an older
|
|
2195
|
+
* server that never Welcomes. Keeps the transport fully back-compatible.
|
|
2196
|
+
*/
|
|
2197
|
+
this.binary = false;
|
|
2102
2198
|
// Reconnect state
|
|
2103
2199
|
/** Set while close() is in progress so the close handler doesn't reconnect. */
|
|
2104
2200
|
this.closed = false;
|
|
@@ -2180,8 +2276,66 @@ class WebSocketClient {
|
|
|
2180
2276
|
this.ws.on("open", () => resolve());
|
|
2181
2277
|
this.ws.on("error", (err) => reject(err));
|
|
2182
2278
|
});
|
|
2279
|
+
// Negotiate the wire format before the dispatcher starts so the Welcome is
|
|
2280
|
+
// consumed here (not by routeMessage), and before any real frame is sent
|
|
2281
|
+
// (resubscribeAll runs only after this resolves).
|
|
2282
|
+
await this.negotiateFormat(this.ws);
|
|
2183
2283
|
this.spawnDispatcher();
|
|
2184
2284
|
}
|
|
2285
|
+
/**
|
|
2286
|
+
* Additive capability handshake: offer msgpack and, if the server Welcomes
|
|
2287
|
+
* it, switch this connection to binary msgpack frames; otherwise stay on JSON
|
|
2288
|
+
* text. The Welcome (a text frame) is read with a one-shot listener and a
|
|
2289
|
+
* timeout so an older server that never answers — or answers with an Error —
|
|
2290
|
+
* simply leaves the connection on JSON. Best-effort and never throws: JSON
|
|
2291
|
+
* always works.
|
|
2292
|
+
*/
|
|
2293
|
+
async negotiateFormat(socket) {
|
|
2294
|
+
this.binary = false;
|
|
2295
|
+
const welcome = await new Promise((resolve) => {
|
|
2296
|
+
const onMsg = (data) => {
|
|
2297
|
+
clearTimeout(timer);
|
|
2298
|
+
try {
|
|
2299
|
+
resolve(JSON.parse(data.toString()));
|
|
2300
|
+
}
|
|
2301
|
+
catch {
|
|
2302
|
+
resolve(null);
|
|
2303
|
+
}
|
|
2304
|
+
};
|
|
2305
|
+
// Only caps the wait when no Welcome comes (a silent/old server); the
|
|
2306
|
+
// listener resolves immediately when it does arrive. 2s comfortably exceeds
|
|
2307
|
+
// the handshake round-trip even on high-latency links.
|
|
2308
|
+
const timer = setTimeout(() => {
|
|
2309
|
+
socket.off("message", onMsg);
|
|
2310
|
+
resolve(null);
|
|
2311
|
+
}, 2000);
|
|
2312
|
+
socket.once("message", onMsg);
|
|
2313
|
+
try {
|
|
2314
|
+
socket.send(JSON.stringify({
|
|
2315
|
+
type: "Hello",
|
|
2316
|
+
payload: { formats: ["msgpack", "json"] },
|
|
2317
|
+
}));
|
|
2318
|
+
}
|
|
2319
|
+
catch {
|
|
2320
|
+
clearTimeout(timer);
|
|
2321
|
+
socket.off("message", onMsg);
|
|
2322
|
+
resolve(null);
|
|
2323
|
+
}
|
|
2324
|
+
});
|
|
2325
|
+
if (welcome &&
|
|
2326
|
+
welcome.type === "Welcome" &&
|
|
2327
|
+
welcome.payload?.format === "msgpack") {
|
|
2328
|
+
this.binary = true;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Send a request object on the active socket using the negotiated format:
|
|
2333
|
+
* binary msgpack when the server Welcomed it, JSON text otherwise. The single
|
|
2334
|
+
* write point so every request honors the negotiated transport.
|
|
2335
|
+
*/
|
|
2336
|
+
sendFrame(obj) {
|
|
2337
|
+
this.ws.send(this.binary ? (0, msgpack_1.encode)(obj) : JSON.stringify(obj));
|
|
2338
|
+
}
|
|
2185
2339
|
spawnDispatcher() {
|
|
2186
2340
|
if (this.dispatcherRunning)
|
|
2187
2341
|
return;
|
|
@@ -2190,11 +2344,16 @@ class WebSocketClient {
|
|
|
2190
2344
|
// socket may still emit late close/error events; ignore them so they don't
|
|
2191
2345
|
// tear down the replacement connection.
|
|
2192
2346
|
const socket = this.ws;
|
|
2193
|
-
socket.on("message", (data) => {
|
|
2347
|
+
socket.on("message", (data, isBinary) => {
|
|
2194
2348
|
if (this.ws !== socket)
|
|
2195
2349
|
return;
|
|
2196
2350
|
try {
|
|
2197
|
-
|
|
2351
|
+
// A binary frame is msgpack (the server only sends binary once it has
|
|
2352
|
+
// Welcomed msgpack); a text frame is JSON. Decode by frame type so the
|
|
2353
|
+
// routed value is identical regardless of negotiated transport.
|
|
2354
|
+
const msg = isBinary
|
|
2355
|
+
? (0, msgpack_1.decode)(data)
|
|
2356
|
+
: JSON.parse(data.toString());
|
|
2198
2357
|
this.routeMessage(msg);
|
|
2199
2358
|
}
|
|
2200
2359
|
catch {
|
|
@@ -2477,7 +2636,7 @@ class WebSocketClient {
|
|
|
2477
2636
|
}
|
|
2478
2637
|
this.pendingRequests.set(messageId, { resolve, reject, timer });
|
|
2479
2638
|
try {
|
|
2480
|
-
this.
|
|
2639
|
+
this.sendFrame(request);
|
|
2481
2640
|
}
|
|
2482
2641
|
catch (err) {
|
|
2483
2642
|
this.pendingRequests.delete(messageId);
|
|
@@ -2560,6 +2719,27 @@ class WebSocketClient {
|
|
|
2560
2719
|
if (stream && !stream.closed) {
|
|
2561
2720
|
stream.close();
|
|
2562
2721
|
}
|
|
2722
|
+
// Best-effort: tell the server to stop streaming this collection (the
|
|
2723
|
+
// server already handles an Unsubscribe frame). If the socket isn't open
|
|
2724
|
+
// the local teardown above suffices, since the server drops subscriptions
|
|
2725
|
+
// when the connection closes. A unique messageId is attached so the
|
|
2726
|
+
// server's Success ack carries a correlation id: it has no pending request
|
|
2727
|
+
// to match, so it is simply ignored — and because the id is present, the
|
|
2728
|
+
// single-pending fallback can't misroute it to an unrelated request.
|
|
2729
|
+
if (this.ws && this.ws.readyState === 1 /* WebSocket.OPEN */) {
|
|
2730
|
+
try {
|
|
2731
|
+
this.sendFrame({
|
|
2732
|
+
type: "Unsubscribe",
|
|
2733
|
+
messageId: this.genMessageId(),
|
|
2734
|
+
payload: { collection },
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
catch {
|
|
2738
|
+
// Best-effort: the socket can close between the readyState check and the
|
|
2739
|
+
// send. Local teardown already happened, so swallow the failure rather
|
|
2740
|
+
// than throw out of a void teardown call.
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2563
2743
|
}
|
|
2564
2744
|
/**
|
|
2565
2745
|
* Send a chat message and receive a streaming response.
|
|
@@ -2588,7 +2768,7 @@ class WebSocketClient {
|
|
|
2588
2768
|
...(options?.excludeTools && { exclude_tools: options.excludeTools }),
|
|
2589
2769
|
},
|
|
2590
2770
|
};
|
|
2591
|
-
this.
|
|
2771
|
+
this.sendFrame(request);
|
|
2592
2772
|
return stream;
|
|
2593
2773
|
}
|
|
2594
2774
|
/**
|
|
@@ -2608,7 +2788,7 @@ class WebSocketClient {
|
|
|
2608
2788
|
resolve: () => resolve(),
|
|
2609
2789
|
reject: (err) => reject(err),
|
|
2610
2790
|
};
|
|
2611
|
-
this.
|
|
2791
|
+
this.sendFrame(request);
|
|
2612
2792
|
});
|
|
2613
2793
|
}
|
|
2614
2794
|
/**
|
|
@@ -2626,7 +2806,24 @@ class WebSocketClient {
|
|
|
2626
2806
|
...(error !== undefined && { error }),
|
|
2627
2807
|
},
|
|
2628
2808
|
};
|
|
2629
|
-
this.
|
|
2809
|
+
this.sendFrame(request);
|
|
2810
|
+
}
|
|
2811
|
+
/**
|
|
2812
|
+
* Cancel an in-flight streaming chat. Fire-and-forget: tells the server to
|
|
2813
|
+
* stop generating tokens for the given chat.
|
|
2814
|
+
*/
|
|
2815
|
+
async cancelChat(chatId) {
|
|
2816
|
+
await this.ensureConnected();
|
|
2817
|
+
// Attach a unique messageId (same generator as unsubscribe). Any Success ack
|
|
2818
|
+
// from the server then carries a correlation id: it has no pending request to
|
|
2819
|
+
// match, so it is ignored — and because the id is present, the dispatcher's
|
|
2820
|
+
// single-pending fallback can't misroute the ack to an unrelated request.
|
|
2821
|
+
const request = {
|
|
2822
|
+
type: "CancelChat",
|
|
2823
|
+
messageId: this.genMessageId(),
|
|
2824
|
+
payload: { chat_id: chatId },
|
|
2825
|
+
};
|
|
2826
|
+
this.sendFrame(request);
|
|
2630
2827
|
}
|
|
2631
2828
|
/**
|
|
2632
2829
|
* Stateless raw LLM completion via WebSocket.
|