@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/src/client.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { encode, decode } from "@msgpack/msgpack";
|
|
6
|
-
import { QueryBuilder, Query
|
|
6
|
+
import { QueryBuilder, Query } from "./query-builder";
|
|
7
7
|
import { SearchQuery, SearchResponse } from "./search";
|
|
8
8
|
import { Schema, SchemaBuilder, CollectionMetadata } from "./schema";
|
|
9
9
|
import { UserFunction, FunctionResult } from "./functions";
|
|
@@ -65,11 +65,11 @@ export class RateLimitError extends Error {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
68
|
+
// `Query` is the canonical find/query body shape — the server's FindBody —
|
|
69
|
+
// re-exported from the query builder so the whole client shares a single type
|
|
70
|
+
// (`filter`, `sort`, `limit`, `skip`, `join`, `select_fields`, `exclude_fields`,
|
|
71
|
+
// matching the server exactly). `QueryBuilder.build()` returns this same type.
|
|
72
|
+
export type { Query };
|
|
73
73
|
|
|
74
74
|
export interface BatchOperationResult {
|
|
75
75
|
successful: string[];
|
|
@@ -115,6 +115,24 @@ export interface FindOptions {
|
|
|
115
115
|
bypassCache?: boolean;
|
|
116
116
|
selectFields?: string[];
|
|
117
117
|
excludeFields?: string[];
|
|
118
|
+
/**
|
|
119
|
+
* Read within a transaction (read-your-writes). When set, the read is served
|
|
120
|
+
* from the transaction's own view — its uncommitted staged writes, else the
|
|
121
|
+
* committed store — and recorded in the transaction's read set for
|
|
122
|
+
* commit-time conflict detection. Omit for an ordinary committed read.
|
|
123
|
+
*/
|
|
124
|
+
transactionId?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Options for a point read by id. `transactionId` enables read-your-writes
|
|
129
|
+
* within a transaction (see {@link FindOptions.transactionId}).
|
|
130
|
+
*/
|
|
131
|
+
export interface FindByIdOptions {
|
|
132
|
+
selectFields?: string[];
|
|
133
|
+
excludeFields?: string[];
|
|
134
|
+
bypassRipple?: boolean;
|
|
135
|
+
transactionId?: string;
|
|
118
136
|
}
|
|
119
137
|
|
|
120
138
|
export interface BatchInsertOptions {
|
|
@@ -782,8 +800,8 @@ export class EkoDBClient {
|
|
|
782
800
|
}
|
|
783
801
|
|
|
784
802
|
const url = params.toString()
|
|
785
|
-
? `/api/insert/${collection}?${params.toString()}`
|
|
786
|
-
: `/api/insert/${collection}`;
|
|
803
|
+
? `/api/insert/${encodeURIComponent(collection)}?${params.toString()}`
|
|
804
|
+
: `/api/insert/${encodeURIComponent(collection)}`;
|
|
787
805
|
|
|
788
806
|
return this.makeRequest<Record>("POST", url, data);
|
|
789
807
|
}
|
|
@@ -813,20 +831,64 @@ export class EkoDBClient {
|
|
|
813
831
|
async find(
|
|
814
832
|
collection: string,
|
|
815
833
|
query: Query | QueryBuilder = {},
|
|
834
|
+
options?: { bypassRipple?: boolean; transactionId?: string },
|
|
816
835
|
): Promise<Record[]> {
|
|
817
836
|
const queryObj = query instanceof QueryBuilder ? query.build() : query;
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
837
|
+
// bypass_ripple and transaction_id are query parameters — the same way every
|
|
838
|
+
// other method (insert/update/findById) carries bypass_ripple — not part of
|
|
839
|
+
// the FindBody. Hoist any bypass_ripple carried on the query object (e.g. from
|
|
840
|
+
// QueryBuilder.bypassRipple()) out of the body so it is ALWAYS sent as a query
|
|
841
|
+
// param; an explicit options.bypassRipple wins.
|
|
842
|
+
let body: unknown = queryObj;
|
|
843
|
+
let bypassRipple = options?.bypassRipple;
|
|
844
|
+
if (body && typeof body === "object" && "bypass_ripple" in body) {
|
|
845
|
+
const { bypass_ripple, ...rest } = body as Record & {
|
|
846
|
+
bypass_ripple?: boolean;
|
|
847
|
+
};
|
|
848
|
+
body = rest;
|
|
849
|
+
if (bypassRipple === undefined) bypassRipple = bypass_ripple;
|
|
850
|
+
}
|
|
851
|
+
const params = new URLSearchParams();
|
|
852
|
+
if (options?.transactionId)
|
|
853
|
+
params.append("transaction_id", options.transactionId);
|
|
854
|
+
if (bypassRipple !== undefined)
|
|
855
|
+
params.append("bypass_ripple", String(bypassRipple));
|
|
856
|
+
const qs = params.toString();
|
|
857
|
+
const url = qs
|
|
858
|
+
? `/api/find/${encodeURIComponent(collection)}?${qs}`
|
|
859
|
+
: `/api/find/${encodeURIComponent(collection)}`;
|
|
860
|
+
return this.makeRequest<Record[]>("POST", url, body);
|
|
823
861
|
}
|
|
824
862
|
|
|
825
863
|
/**
|
|
826
|
-
* Find a document by ID
|
|
864
|
+
* Find a document by ID.
|
|
865
|
+
* @param options - Optional read options. `transactionId` reads within a
|
|
866
|
+
* transaction (read-your-writes); see {@link FindByIdOptions}.
|
|
827
867
|
*/
|
|
828
|
-
async findById(
|
|
829
|
-
|
|
868
|
+
async findById(
|
|
869
|
+
collection: string,
|
|
870
|
+
id: string,
|
|
871
|
+
options?: FindByIdOptions,
|
|
872
|
+
): Promise<Record> {
|
|
873
|
+
const params = new URLSearchParams();
|
|
874
|
+
if (options?.selectFields?.length) {
|
|
875
|
+
params.append("select_fields", options.selectFields.join(","));
|
|
876
|
+
}
|
|
877
|
+
if (options?.excludeFields?.length) {
|
|
878
|
+
params.append("exclude_fields", options.excludeFields.join(","));
|
|
879
|
+
}
|
|
880
|
+
// bypass_ripple is a GET query param, the same way the non-transactional
|
|
881
|
+
// findById carries it; it rides alongside transaction_id when both are set.
|
|
882
|
+
if (options?.bypassRipple !== undefined) {
|
|
883
|
+
params.append("bypass_ripple", String(options.bypassRipple));
|
|
884
|
+
}
|
|
885
|
+
if (options?.transactionId) {
|
|
886
|
+
params.append("transaction_id", options.transactionId);
|
|
887
|
+
}
|
|
888
|
+
const url = params.toString()
|
|
889
|
+
? `/api/find/${encodeURIComponent(collection)}/${encodeURIComponent(id)}?${params.toString()}`
|
|
890
|
+
: `/api/find/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`;
|
|
891
|
+
return this.makeRequest<Record>("GET", url);
|
|
830
892
|
}
|
|
831
893
|
|
|
832
894
|
/**
|
|
@@ -835,12 +897,14 @@ export class EkoDBClient {
|
|
|
835
897
|
* @param id - Document ID
|
|
836
898
|
* @param selectFields - Fields to include in the result
|
|
837
899
|
* @param excludeFields - Fields to exclude from the result
|
|
900
|
+
* @param transactionId - Read within a transaction (read-your-writes)
|
|
838
901
|
*/
|
|
839
902
|
async findByIdWithProjection(
|
|
840
903
|
collection: string,
|
|
841
904
|
id: string,
|
|
842
905
|
selectFields?: string[],
|
|
843
906
|
excludeFields?: string[],
|
|
907
|
+
transactionId?: string,
|
|
844
908
|
): Promise<Record> {
|
|
845
909
|
const params = new URLSearchParams();
|
|
846
910
|
if (selectFields?.length) {
|
|
@@ -849,9 +913,12 @@ export class EkoDBClient {
|
|
|
849
913
|
if (excludeFields?.length) {
|
|
850
914
|
params.append("exclude_fields", excludeFields.join(","));
|
|
851
915
|
}
|
|
916
|
+
if (transactionId) {
|
|
917
|
+
params.append("transaction_id", transactionId);
|
|
918
|
+
}
|
|
852
919
|
const url = params.toString()
|
|
853
|
-
? `/api/find/${collection}/${id}?${params.toString()}`
|
|
854
|
-
: `/api/find/${collection}/${id}`;
|
|
920
|
+
? `/api/find/${encodeURIComponent(collection)}/${encodeURIComponent(id)}?${params.toString()}`
|
|
921
|
+
: `/api/find/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`;
|
|
855
922
|
return this.makeRequest<Record>("GET", url);
|
|
856
923
|
}
|
|
857
924
|
|
|
@@ -877,8 +944,8 @@ export class EkoDBClient {
|
|
|
877
944
|
}
|
|
878
945
|
|
|
879
946
|
const url = params.toString()
|
|
880
|
-
? `/api/update/${collection}/${id}?${params.toString()}`
|
|
881
|
-
: `/api/update/${collection}/${id}`;
|
|
947
|
+
? `/api/update/${encodeURIComponent(collection)}/${encodeURIComponent(id)}?${params.toString()}`
|
|
948
|
+
: `/api/update/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`;
|
|
882
949
|
|
|
883
950
|
return this.makeRequest<Record>("PUT", url, record);
|
|
884
951
|
}
|
|
@@ -903,7 +970,7 @@ export class EkoDBClient {
|
|
|
903
970
|
field: string,
|
|
904
971
|
value?: any,
|
|
905
972
|
): Promise<Record> {
|
|
906
|
-
const url = `/api/update/${collection}/${id}/action/${action}`;
|
|
973
|
+
const url = `/api/update/${encodeURIComponent(collection)}/${encodeURIComponent(id)}/action/${encodeURIComponent(action)}`;
|
|
907
974
|
return this.makeRequest<Record>("PUT", url, {
|
|
908
975
|
field,
|
|
909
976
|
value: value ?? null,
|
|
@@ -925,7 +992,7 @@ export class EkoDBClient {
|
|
|
925
992
|
id: string,
|
|
926
993
|
actions: [string, string, any][],
|
|
927
994
|
): Promise<Record> {
|
|
928
|
-
const url = `/api/update/sequence/${collection}/${id}`;
|
|
995
|
+
const url = `/api/update/sequence/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`;
|
|
929
996
|
return this.makeRequest<Record>("PUT", url, actions);
|
|
930
997
|
}
|
|
931
998
|
|
|
@@ -949,8 +1016,8 @@ export class EkoDBClient {
|
|
|
949
1016
|
}
|
|
950
1017
|
|
|
951
1018
|
const url = params.toString()
|
|
952
|
-
? `/api/delete/${collection}/${id}?${params.toString()}`
|
|
953
|
-
: `/api/delete/${collection}/${id}`;
|
|
1019
|
+
? `/api/delete/${encodeURIComponent(collection)}/${encodeURIComponent(id)}?${params.toString()}`
|
|
1020
|
+
: `/api/delete/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`;
|
|
954
1021
|
|
|
955
1022
|
await this.makeRequest<void>("DELETE", url);
|
|
956
1023
|
}
|
|
@@ -976,8 +1043,8 @@ export class EkoDBClient {
|
|
|
976
1043
|
|
|
977
1044
|
const inserts = records.map((data) => ({ data }));
|
|
978
1045
|
const url = params.toString()
|
|
979
|
-
? `/api/batch/insert/${collection}?${params.toString()}`
|
|
980
|
-
: `/api/batch/insert/${collection}`;
|
|
1046
|
+
? `/api/batch/insert/${encodeURIComponent(collection)}?${params.toString()}`
|
|
1047
|
+
: `/api/batch/insert/${encodeURIComponent(collection)}`;
|
|
981
1048
|
|
|
982
1049
|
return this.makeRequest<BatchOperationResult>("POST", url, { inserts });
|
|
983
1050
|
}
|
|
@@ -996,7 +1063,7 @@ export class EkoDBClient {
|
|
|
996
1063
|
}));
|
|
997
1064
|
return this.makeRequest<BatchOperationResult>(
|
|
998
1065
|
"PUT",
|
|
999
|
-
`/api/batch/update/${collection}`,
|
|
1066
|
+
`/api/batch/update/${encodeURIComponent(collection)}`,
|
|
1000
1067
|
{ updates: formattedUpdates },
|
|
1001
1068
|
);
|
|
1002
1069
|
}
|
|
@@ -1015,7 +1082,7 @@ export class EkoDBClient {
|
|
|
1015
1082
|
}));
|
|
1016
1083
|
return this.makeRequest<BatchOperationResult>(
|
|
1017
1084
|
"DELETE",
|
|
1018
|
-
`/api/batch/delete/${collection}`,
|
|
1085
|
+
`/api/batch/delete/${encodeURIComponent(collection)}`,
|
|
1019
1086
|
{ deletes },
|
|
1020
1087
|
);
|
|
1021
1088
|
}
|
|
@@ -1067,6 +1134,19 @@ export class EkoDBClient {
|
|
|
1067
1134
|
);
|
|
1068
1135
|
}
|
|
1069
1136
|
|
|
1137
|
+
/**
|
|
1138
|
+
* Clear the entire KV store (all keys in the namespace).
|
|
1139
|
+
*/
|
|
1140
|
+
async kvClear(): Promise<void> {
|
|
1141
|
+
await this.makeRequest<void>(
|
|
1142
|
+
"DELETE",
|
|
1143
|
+
"/api/kv/clear",
|
|
1144
|
+
undefined,
|
|
1145
|
+
0,
|
|
1146
|
+
true, // Force JSON for KV operations
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1070
1150
|
/**
|
|
1071
1151
|
* Batch get multiple keys
|
|
1072
1152
|
* @param keys - Array of keys to retrieve
|
|
@@ -1171,7 +1251,18 @@ export class EkoDBClient {
|
|
|
1171
1251
|
// ============================================================================
|
|
1172
1252
|
|
|
1173
1253
|
/**
|
|
1174
|
-
* Begin a new transaction
|
|
1254
|
+
* Begin a new transaction.
|
|
1255
|
+
*
|
|
1256
|
+
* Transactions are buffered: statements issued with this `transactionId`
|
|
1257
|
+
* (passed via the `transactionId` option on insert/update/delete/find/…) are
|
|
1258
|
+
* staged and applied atomically only at {@link commitTransaction}. They are
|
|
1259
|
+
* invisible to everyone else until commit, and visible to this transaction's
|
|
1260
|
+
* own reads (read-your-writes) only when those reads also carry the
|
|
1261
|
+
* `transactionId`. {@link rollbackTransaction} discards the staged writes.
|
|
1262
|
+
* `commitTransaction` may reject with a conflict (HTTP 409) if a record this
|
|
1263
|
+
* transaction read or wrote was changed by another committed transaction —
|
|
1264
|
+
* retry the transaction in that case.
|
|
1265
|
+
*
|
|
1175
1266
|
* @param isolationLevel - Transaction isolation level (default: "ReadCommitted")
|
|
1176
1267
|
* @returns Transaction ID
|
|
1177
1268
|
*/
|
|
@@ -1220,7 +1311,7 @@ export class EkoDBClient {
|
|
|
1220
1311
|
}
|
|
1221
1312
|
|
|
1222
1313
|
/**
|
|
1223
|
-
* Rollback a transaction
|
|
1314
|
+
* Rollback a transaction (discards all staged writes; nothing was applied).
|
|
1224
1315
|
* @param transactionId - The transaction ID to rollback
|
|
1225
1316
|
*/
|
|
1226
1317
|
async rollbackTransaction(transactionId: string): Promise<void> {
|
|
@@ -1233,6 +1324,49 @@ export class EkoDBClient {
|
|
|
1233
1324
|
);
|
|
1234
1325
|
}
|
|
1235
1326
|
|
|
1327
|
+
/**
|
|
1328
|
+
* Create a savepoint within a transaction. A later
|
|
1329
|
+
* {@link rollbackToSavepoint} discards everything staged after it.
|
|
1330
|
+
*/
|
|
1331
|
+
async createSavepoint(transactionId: string, name: string): Promise<void> {
|
|
1332
|
+
await this.makeRequest<void>(
|
|
1333
|
+
"POST",
|
|
1334
|
+
`/api/transactions/${encodeURIComponent(transactionId)}/savepoints`,
|
|
1335
|
+
{ name },
|
|
1336
|
+
0,
|
|
1337
|
+
true,
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* Roll the transaction back to a savepoint, discarding writes staged after it.
|
|
1343
|
+
*/
|
|
1344
|
+
async rollbackToSavepoint(
|
|
1345
|
+
transactionId: string,
|
|
1346
|
+
name: string,
|
|
1347
|
+
): Promise<void> {
|
|
1348
|
+
await this.makeRequest<void>(
|
|
1349
|
+
"POST",
|
|
1350
|
+
`/api/transactions/${encodeURIComponent(transactionId)}/savepoints/${encodeURIComponent(name)}/rollback`,
|
|
1351
|
+
undefined,
|
|
1352
|
+
0,
|
|
1353
|
+
true,
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Release (forget) a savepoint. Staged work is unaffected.
|
|
1359
|
+
*/
|
|
1360
|
+
async releaseSavepoint(transactionId: string, name: string): Promise<void> {
|
|
1361
|
+
await this.makeRequest<void>(
|
|
1362
|
+
"DELETE",
|
|
1363
|
+
`/api/transactions/${encodeURIComponent(transactionId)}/savepoints/${encodeURIComponent(name)}`,
|
|
1364
|
+
undefined,
|
|
1365
|
+
0,
|
|
1366
|
+
true,
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1236
1370
|
// ============================================================================
|
|
1237
1371
|
// Convenience Methods
|
|
1238
1372
|
// ============================================================================
|
|
@@ -1379,7 +1513,7 @@ export class EkoDBClient {
|
|
|
1379
1513
|
): Promise<Record[]> {
|
|
1380
1514
|
// Page 1 = skip 0, Page 2 = skip pageSize, etc.
|
|
1381
1515
|
const skip = page > 0 ? (page - 1) * pageSize : 0;
|
|
1382
|
-
const query:
|
|
1516
|
+
const query: Query = {
|
|
1383
1517
|
limit: pageSize,
|
|
1384
1518
|
skip: skip,
|
|
1385
1519
|
};
|
|
@@ -1400,13 +1534,27 @@ export class EkoDBClient {
|
|
|
1400
1534
|
return result.collections;
|
|
1401
1535
|
}
|
|
1402
1536
|
|
|
1537
|
+
/**
|
|
1538
|
+
* List collections, excluding internal chat/system collections.
|
|
1539
|
+
*/
|
|
1540
|
+
async listUserCollections(): Promise<string[]> {
|
|
1541
|
+
const result = await this.makeRequest<{ collections: string[] }>(
|
|
1542
|
+
"GET",
|
|
1543
|
+
"/api/collections?exclude_internal=true",
|
|
1544
|
+
undefined,
|
|
1545
|
+
0,
|
|
1546
|
+
true, // Force JSON for metadata operations
|
|
1547
|
+
);
|
|
1548
|
+
return result.collections;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1403
1551
|
/**
|
|
1404
1552
|
* Delete a collection
|
|
1405
1553
|
*/
|
|
1406
1554
|
async deleteCollection(collection: string): Promise<void> {
|
|
1407
1555
|
await this.makeRequest<void>(
|
|
1408
1556
|
"DELETE",
|
|
1409
|
-
`/api/collections/${collection}`,
|
|
1557
|
+
`/api/collections/${encodeURIComponent(collection)}`,
|
|
1410
1558
|
undefined,
|
|
1411
1559
|
0,
|
|
1412
1560
|
true, // Force JSON for metadata operations
|
|
@@ -1424,7 +1572,7 @@ export class EkoDBClient {
|
|
|
1424
1572
|
async restoreRecord(collection: string, id: string): Promise<boolean> {
|
|
1425
1573
|
const result = await this.makeRequest<{ status: string }>(
|
|
1426
1574
|
"POST",
|
|
1427
|
-
`/api/trash/${collection}/${id}`,
|
|
1575
|
+
`/api/trash/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`,
|
|
1428
1576
|
undefined,
|
|
1429
1577
|
0,
|
|
1430
1578
|
true,
|
|
@@ -1446,7 +1594,13 @@ export class EkoDBClient {
|
|
|
1446
1594
|
status: string;
|
|
1447
1595
|
collection: string;
|
|
1448
1596
|
records_restored: number;
|
|
1449
|
-
}>(
|
|
1597
|
+
}>(
|
|
1598
|
+
"POST",
|
|
1599
|
+
`/api/trash/${encodeURIComponent(collection)}`,
|
|
1600
|
+
undefined,
|
|
1601
|
+
0,
|
|
1602
|
+
true,
|
|
1603
|
+
);
|
|
1450
1604
|
return { recordsRestored: result.records_restored };
|
|
1451
1605
|
}
|
|
1452
1606
|
|
|
@@ -1473,7 +1627,7 @@ export class EkoDBClient {
|
|
|
1473
1627
|
const schemaObj = schema instanceof SchemaBuilder ? schema.build() : schema;
|
|
1474
1628
|
await this.makeRequest<void>(
|
|
1475
1629
|
"POST",
|
|
1476
|
-
`/api/collections/${collection}`,
|
|
1630
|
+
`/api/collections/${encodeURIComponent(collection)}`,
|
|
1477
1631
|
schemaObj,
|
|
1478
1632
|
0,
|
|
1479
1633
|
true, // Force JSON for metadata operations
|
|
@@ -1489,7 +1643,7 @@ export class EkoDBClient {
|
|
|
1489
1643
|
async getCollection(collection: string): Promise<CollectionMetadata> {
|
|
1490
1644
|
return this.makeRequest<CollectionMetadata>(
|
|
1491
1645
|
"GET",
|
|
1492
|
-
`/api/collections/${collection}`,
|
|
1646
|
+
`/api/collections/${encodeURIComponent(collection)}`,
|
|
1493
1647
|
undefined,
|
|
1494
1648
|
0,
|
|
1495
1649
|
true, // Force JSON for metadata operations
|
|
@@ -1547,7 +1701,7 @@ export class EkoDBClient {
|
|
|
1547
1701
|
// Ensure all parameters from SearchQuery are sent to server
|
|
1548
1702
|
return this.makeRequest<SearchResponse>(
|
|
1549
1703
|
"POST",
|
|
1550
|
-
`/api/search/${collection}`,
|
|
1704
|
+
`/api/search/${encodeURIComponent(collection)}`,
|
|
1551
1705
|
query,
|
|
1552
1706
|
0,
|
|
1553
1707
|
true, // Force JSON for search operations
|
|
@@ -1592,7 +1746,7 @@ export class EkoDBClient {
|
|
|
1592
1746
|
|
|
1593
1747
|
return this.makeRequest<DistinctValuesResponse>(
|
|
1594
1748
|
"POST",
|
|
1595
|
-
`/api/distinct/${collection}/${field}`,
|
|
1749
|
+
`/api/distinct/${encodeURIComponent(collection)}/${encodeURIComponent(field)}`,
|
|
1596
1750
|
body,
|
|
1597
1751
|
0,
|
|
1598
1752
|
true, // Force JSON
|
|
@@ -1828,7 +1982,7 @@ export class EkoDBClient {
|
|
|
1828
1982
|
): Promise<ChatResponse> {
|
|
1829
1983
|
return this.makeRequest<ChatResponse>(
|
|
1830
1984
|
"POST",
|
|
1831
|
-
`/api/chat/${sessionId}/messages`,
|
|
1985
|
+
`/api/chat/${encodeURIComponent(sessionId)}/messages`,
|
|
1832
1986
|
request,
|
|
1833
1987
|
0,
|
|
1834
1988
|
true, // Force JSON for chat operations
|
|
@@ -1848,7 +2002,7 @@ export class EkoDBClient {
|
|
|
1848
2002
|
): Promise<void> {
|
|
1849
2003
|
await this.makeRequest(
|
|
1850
2004
|
"POST",
|
|
1851
|
-
`/api/chat/${chatId}/tool-result`,
|
|
2005
|
+
`/api/chat/${encodeURIComponent(chatId)}/tool-result`,
|
|
1852
2006
|
{
|
|
1853
2007
|
call_id: callId,
|
|
1854
2008
|
success,
|
|
@@ -1884,7 +2038,7 @@ export class EkoDBClient {
|
|
|
1884
2038
|
await this.refreshToken();
|
|
1885
2039
|
token = await this.getToken();
|
|
1886
2040
|
}
|
|
1887
|
-
const url = `${this.baseURL}/api/chat/${chatId}/messages/stream`;
|
|
2041
|
+
const url = `${this.baseURL}/api/chat/${encodeURIComponent(chatId)}/messages/stream`;
|
|
1888
2042
|
|
|
1889
2043
|
const response = await fetch(url, {
|
|
1890
2044
|
method: "POST",
|
|
@@ -1979,7 +2133,7 @@ export class EkoDBClient {
|
|
|
1979
2133
|
async getChatSession(sessionId: string): Promise<ChatSessionResponse> {
|
|
1980
2134
|
return this.makeRequest<ChatSessionResponse>(
|
|
1981
2135
|
"GET",
|
|
1982
|
-
`/api/chat/${sessionId}`,
|
|
2136
|
+
`/api/chat/${encodeURIComponent(sessionId)}`,
|
|
1983
2137
|
undefined,
|
|
1984
2138
|
0,
|
|
1985
2139
|
true, // Force JSON for chat operations
|
|
@@ -2022,8 +2176,8 @@ export class EkoDBClient {
|
|
|
2022
2176
|
|
|
2023
2177
|
const queryString = params.toString();
|
|
2024
2178
|
const path = queryString
|
|
2025
|
-
? `/api/chat/${sessionId}/messages?${queryString}`
|
|
2026
|
-
: `/api/chat/${sessionId}/messages`;
|
|
2179
|
+
? `/api/chat/${encodeURIComponent(sessionId)}/messages?${queryString}`
|
|
2180
|
+
: `/api/chat/${encodeURIComponent(sessionId)}/messages`;
|
|
2027
2181
|
return this.makeRequest<GetMessagesResponse>(
|
|
2028
2182
|
"GET",
|
|
2029
2183
|
path,
|
|
@@ -2042,7 +2196,7 @@ export class EkoDBClient {
|
|
|
2042
2196
|
): Promise<ChatSessionResponse> {
|
|
2043
2197
|
return this.makeRequest<ChatSessionResponse>(
|
|
2044
2198
|
"PUT",
|
|
2045
|
-
`/api/chat/${sessionId}`,
|
|
2199
|
+
`/api/chat/${encodeURIComponent(sessionId)}`,
|
|
2046
2200
|
request,
|
|
2047
2201
|
0,
|
|
2048
2202
|
true, // Force JSON for chat operations
|
|
@@ -2070,7 +2224,7 @@ export class EkoDBClient {
|
|
|
2070
2224
|
async deleteChatSession(sessionId: string): Promise<void> {
|
|
2071
2225
|
await this.makeRequest<void>(
|
|
2072
2226
|
"DELETE",
|
|
2073
|
-
`/api/chat/${sessionId}`,
|
|
2227
|
+
`/api/chat/${encodeURIComponent(sessionId)}`,
|
|
2074
2228
|
undefined,
|
|
2075
2229
|
0,
|
|
2076
2230
|
true, // Force JSON for chat operations
|
|
@@ -2086,7 +2240,7 @@ export class EkoDBClient {
|
|
|
2086
2240
|
): Promise<ChatResponse> {
|
|
2087
2241
|
return this.makeRequest<ChatResponse>(
|
|
2088
2242
|
"POST",
|
|
2089
|
-
`/api/chat/${sessionId}/messages/${messageId}/regenerate`,
|
|
2243
|
+
`/api/chat/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}/regenerate`,
|
|
2090
2244
|
undefined,
|
|
2091
2245
|
0,
|
|
2092
2246
|
true, // Force JSON for chat operations
|
|
@@ -2103,7 +2257,7 @@ export class EkoDBClient {
|
|
|
2103
2257
|
): Promise<void> {
|
|
2104
2258
|
await this.makeRequest<void>(
|
|
2105
2259
|
"PUT",
|
|
2106
|
-
`/api/chat/${sessionId}/messages/${messageId}`,
|
|
2260
|
+
`/api/chat/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}`,
|
|
2107
2261
|
{ content },
|
|
2108
2262
|
0,
|
|
2109
2263
|
true, // Force JSON for chat operations
|
|
@@ -2116,7 +2270,7 @@ export class EkoDBClient {
|
|
|
2116
2270
|
async deleteChatMessage(sessionId: string, messageId: string): Promise<void> {
|
|
2117
2271
|
await this.makeRequest<void>(
|
|
2118
2272
|
"DELETE",
|
|
2119
|
-
`/api/chat/${sessionId}/messages/${messageId}`,
|
|
2273
|
+
`/api/chat/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}`,
|
|
2120
2274
|
undefined,
|
|
2121
2275
|
0,
|
|
2122
2276
|
true, // Force JSON for chat operations
|
|
@@ -2133,7 +2287,7 @@ export class EkoDBClient {
|
|
|
2133
2287
|
): Promise<void> {
|
|
2134
2288
|
await this.makeRequest<void>(
|
|
2135
2289
|
"PATCH",
|
|
2136
|
-
`/api/chat/${sessionId}/messages/${messageId}/forgotten`,
|
|
2290
|
+
`/api/chat/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}/forgotten`,
|
|
2137
2291
|
{ forgotten },
|
|
2138
2292
|
0,
|
|
2139
2293
|
true, // Force JSON for chat operations
|
|
@@ -2160,7 +2314,7 @@ export class EkoDBClient {
|
|
|
2160
2314
|
}
|
|
2161
2315
|
return this.makeRequest<CompactChatResponse>(
|
|
2162
2316
|
"POST",
|
|
2163
|
-
`/api/chat/${chatId}/compact`,
|
|
2317
|
+
`/api/chat/${encodeURIComponent(chatId)}/compact`,
|
|
2164
2318
|
body,
|
|
2165
2319
|
0,
|
|
2166
2320
|
true, // Force JSON for chat operations
|
|
@@ -2235,7 +2389,7 @@ export class EkoDBClient {
|
|
|
2235
2389
|
async getChatMessage(sessionId: string, messageId: string): Promise<Record> {
|
|
2236
2390
|
return this.makeRequest<Record>(
|
|
2237
2391
|
"GET",
|
|
2238
|
-
`/api/chat/${sessionId}/messages/${messageId}`,
|
|
2392
|
+
`/api/chat/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(messageId)}`,
|
|
2239
2393
|
undefined,
|
|
2240
2394
|
0,
|
|
2241
2395
|
true, // Force JSON for chat operations
|
|
@@ -2262,7 +2416,10 @@ export class EkoDBClient {
|
|
|
2262
2416
|
* Get a function by ID
|
|
2263
2417
|
*/
|
|
2264
2418
|
async getFunction(id: string): Promise<UserFunction> {
|
|
2265
|
-
return this.makeRequest<UserFunction>(
|
|
2419
|
+
return this.makeRequest<UserFunction>(
|
|
2420
|
+
"GET",
|
|
2421
|
+
`/api/functions/${encodeURIComponent(id)}`,
|
|
2422
|
+
);
|
|
2266
2423
|
}
|
|
2267
2424
|
|
|
2268
2425
|
/**
|
|
@@ -2277,14 +2434,21 @@ export class EkoDBClient {
|
|
|
2277
2434
|
* Update an existing function by ID
|
|
2278
2435
|
*/
|
|
2279
2436
|
async updateFunction(id: string, script: UserFunction): Promise<void> {
|
|
2280
|
-
await this.makeRequest<void>(
|
|
2437
|
+
await this.makeRequest<void>(
|
|
2438
|
+
"PUT",
|
|
2439
|
+
`/api/functions/${encodeURIComponent(id)}`,
|
|
2440
|
+
script,
|
|
2441
|
+
);
|
|
2281
2442
|
}
|
|
2282
2443
|
|
|
2283
2444
|
/**
|
|
2284
2445
|
* Delete a function by ID
|
|
2285
2446
|
*/
|
|
2286
2447
|
async deleteFunction(id: string): Promise<void> {
|
|
2287
|
-
await this.makeRequest<void>(
|
|
2448
|
+
await this.makeRequest<void>(
|
|
2449
|
+
"DELETE",
|
|
2450
|
+
`/api/functions/${encodeURIComponent(id)}`,
|
|
2451
|
+
);
|
|
2288
2452
|
}
|
|
2289
2453
|
|
|
2290
2454
|
/**
|
|
@@ -2296,7 +2460,7 @@ export class EkoDBClient {
|
|
|
2296
2460
|
): Promise<FunctionResult> {
|
|
2297
2461
|
return this.makeRequest<FunctionResult>(
|
|
2298
2462
|
"POST",
|
|
2299
|
-
`/api/functions/${idOrLabel}`,
|
|
2463
|
+
`/api/functions/${encodeURIComponent(idOrLabel)}`,
|
|
2300
2464
|
params || {},
|
|
2301
2465
|
);
|
|
2302
2466
|
}
|
|
@@ -2543,7 +2707,7 @@ export class EkoDBClient {
|
|
|
2543
2707
|
async goalStepStart(id: string, stepIndex: number): Promise<Record> {
|
|
2544
2708
|
return this.makeRequest<Record>(
|
|
2545
2709
|
"POST",
|
|
2546
|
-
`/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/start`,
|
|
2710
|
+
`/api/chat/goals/${encodeURIComponent(id)}/steps/${encodeURIComponent(String(stepIndex))}/start`,
|
|
2547
2711
|
undefined,
|
|
2548
2712
|
0,
|
|
2549
2713
|
true,
|
|
@@ -2558,7 +2722,7 @@ export class EkoDBClient {
|
|
|
2558
2722
|
): Promise<Record> {
|
|
2559
2723
|
return this.makeRequest<Record>(
|
|
2560
2724
|
"POST",
|
|
2561
|
-
`/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/complete`,
|
|
2725
|
+
`/api/chat/goals/${encodeURIComponent(id)}/steps/${encodeURIComponent(String(stepIndex))}/complete`,
|
|
2562
2726
|
data,
|
|
2563
2727
|
0,
|
|
2564
2728
|
true,
|
|
@@ -2573,7 +2737,7 @@ export class EkoDBClient {
|
|
|
2573
2737
|
): Promise<Record> {
|
|
2574
2738
|
return this.makeRequest<Record>(
|
|
2575
2739
|
"POST",
|
|
2576
|
-
`/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/fail`,
|
|
2740
|
+
`/api/chat/goals/${encodeURIComponent(id)}/steps/${encodeURIComponent(String(stepIndex))}/fail`,
|
|
2577
2741
|
data,
|
|
2578
2742
|
0,
|
|
2579
2743
|
true,
|
|
@@ -3446,6 +3610,13 @@ export class WebSocketClient {
|
|
|
3446
3610
|
private ws: any = null;
|
|
3447
3611
|
private dispatcherRunning = false;
|
|
3448
3612
|
private schemaCache: SchemaCache | null = null;
|
|
3613
|
+
/**
|
|
3614
|
+
* Per-connection wire format, set by negotiateFormat() on every (re)connect:
|
|
3615
|
+
* true once the server has Welcomed msgpack, so frames are sent/received as
|
|
3616
|
+
* binary msgpack; false (JSON text) otherwise, including against an older
|
|
3617
|
+
* server that never Welcomes. Keeps the transport fully back-compatible.
|
|
3618
|
+
*/
|
|
3619
|
+
private binary = false;
|
|
3449
3620
|
|
|
3450
3621
|
// Reconnect config
|
|
3451
3622
|
private autoReconnect: boolean;
|
|
@@ -3572,9 +3743,72 @@ export class WebSocketClient {
|
|
|
3572
3743
|
this.ws.on("error", (err: Error) => reject(err));
|
|
3573
3744
|
});
|
|
3574
3745
|
|
|
3746
|
+
// Negotiate the wire format before the dispatcher starts so the Welcome is
|
|
3747
|
+
// consumed here (not by routeMessage), and before any real frame is sent
|
|
3748
|
+
// (resubscribeAll runs only after this resolves).
|
|
3749
|
+
await this.negotiateFormat(this.ws);
|
|
3750
|
+
|
|
3575
3751
|
this.spawnDispatcher();
|
|
3576
3752
|
}
|
|
3577
3753
|
|
|
3754
|
+
/**
|
|
3755
|
+
* Additive capability handshake: offer msgpack and, if the server Welcomes
|
|
3756
|
+
* it, switch this connection to binary msgpack frames; otherwise stay on JSON
|
|
3757
|
+
* text. The Welcome (a text frame) is read with a one-shot listener and a
|
|
3758
|
+
* timeout so an older server that never answers — or answers with an Error —
|
|
3759
|
+
* simply leaves the connection on JSON. Best-effort and never throws: JSON
|
|
3760
|
+
* always works.
|
|
3761
|
+
*/
|
|
3762
|
+
private async negotiateFormat(socket: any): Promise<void> {
|
|
3763
|
+
this.binary = false;
|
|
3764
|
+
const welcome = await new Promise<any | null>((resolve) => {
|
|
3765
|
+
const onMsg = (data: Buffer) => {
|
|
3766
|
+
clearTimeout(timer);
|
|
3767
|
+
try {
|
|
3768
|
+
resolve(JSON.parse(data.toString()));
|
|
3769
|
+
} catch {
|
|
3770
|
+
resolve(null);
|
|
3771
|
+
}
|
|
3772
|
+
};
|
|
3773
|
+
// Only caps the wait when no Welcome comes (a silent/old server); the
|
|
3774
|
+
// listener resolves immediately when it does arrive. 2s comfortably exceeds
|
|
3775
|
+
// the handshake round-trip even on high-latency links.
|
|
3776
|
+
const timer = setTimeout(() => {
|
|
3777
|
+
socket.off("message", onMsg);
|
|
3778
|
+
resolve(null);
|
|
3779
|
+
}, 2000);
|
|
3780
|
+
socket.once("message", onMsg);
|
|
3781
|
+
try {
|
|
3782
|
+
socket.send(
|
|
3783
|
+
JSON.stringify({
|
|
3784
|
+
type: "Hello",
|
|
3785
|
+
payload: { formats: ["msgpack", "json"] },
|
|
3786
|
+
}),
|
|
3787
|
+
);
|
|
3788
|
+
} catch {
|
|
3789
|
+
clearTimeout(timer);
|
|
3790
|
+
socket.off("message", onMsg);
|
|
3791
|
+
resolve(null);
|
|
3792
|
+
}
|
|
3793
|
+
});
|
|
3794
|
+
if (
|
|
3795
|
+
welcome &&
|
|
3796
|
+
welcome.type === "Welcome" &&
|
|
3797
|
+
welcome.payload?.format === "msgpack"
|
|
3798
|
+
) {
|
|
3799
|
+
this.binary = true;
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
/**
|
|
3804
|
+
* Send a request object on the active socket using the negotiated format:
|
|
3805
|
+
* binary msgpack when the server Welcomed it, JSON text otherwise. The single
|
|
3806
|
+
* write point so every request honors the negotiated transport.
|
|
3807
|
+
*/
|
|
3808
|
+
private sendFrame(obj: any): void {
|
|
3809
|
+
this.ws.send(this.binary ? encode(obj) : JSON.stringify(obj));
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3578
3812
|
private spawnDispatcher(): void {
|
|
3579
3813
|
if (this.dispatcherRunning) return;
|
|
3580
3814
|
this.dispatcherRunning = true;
|
|
@@ -3584,10 +3818,15 @@ export class WebSocketClient {
|
|
|
3584
3818
|
// tear down the replacement connection.
|
|
3585
3819
|
const socket = this.ws;
|
|
3586
3820
|
|
|
3587
|
-
socket.on("message", (data: Buffer) => {
|
|
3821
|
+
socket.on("message", (data: Buffer, isBinary: boolean) => {
|
|
3588
3822
|
if (this.ws !== socket) return;
|
|
3589
3823
|
try {
|
|
3590
|
-
|
|
3824
|
+
// A binary frame is msgpack (the server only sends binary once it has
|
|
3825
|
+
// Welcomed msgpack); a text frame is JSON. Decode by frame type so the
|
|
3826
|
+
// routed value is identical regardless of negotiated transport.
|
|
3827
|
+
const msg = isBinary
|
|
3828
|
+
? (decode(data) as any)
|
|
3829
|
+
: JSON.parse(data.toString());
|
|
3591
3830
|
this.routeMessage(msg);
|
|
3592
3831
|
} catch {
|
|
3593
3832
|
// Ignore malformed messages
|
|
@@ -3897,7 +4136,7 @@ export class WebSocketClient {
|
|
|
3897
4136
|
|
|
3898
4137
|
this.pendingRequests.set(messageId, { resolve, reject, timer });
|
|
3899
4138
|
try {
|
|
3900
|
-
this.
|
|
4139
|
+
this.sendFrame(request);
|
|
3901
4140
|
} catch (err) {
|
|
3902
4141
|
this.pendingRequests.delete(messageId);
|
|
3903
4142
|
if (timer) clearTimeout(timer);
|
|
@@ -3989,6 +4228,26 @@ export class WebSocketClient {
|
|
|
3989
4228
|
if (stream && !stream.closed) {
|
|
3990
4229
|
stream.close();
|
|
3991
4230
|
}
|
|
4231
|
+
// Best-effort: tell the server to stop streaming this collection (the
|
|
4232
|
+
// server already handles an Unsubscribe frame). If the socket isn't open
|
|
4233
|
+
// the local teardown above suffices, since the server drops subscriptions
|
|
4234
|
+
// when the connection closes. A unique messageId is attached so the
|
|
4235
|
+
// server's Success ack carries a correlation id: it has no pending request
|
|
4236
|
+
// to match, so it is simply ignored — and because the id is present, the
|
|
4237
|
+
// single-pending fallback can't misroute it to an unrelated request.
|
|
4238
|
+
if (this.ws && this.ws.readyState === 1 /* WebSocket.OPEN */) {
|
|
4239
|
+
try {
|
|
4240
|
+
this.sendFrame({
|
|
4241
|
+
type: "Unsubscribe",
|
|
4242
|
+
messageId: this.genMessageId(),
|
|
4243
|
+
payload: { collection },
|
|
4244
|
+
});
|
|
4245
|
+
} catch {
|
|
4246
|
+
// Best-effort: the socket can close between the readyState check and the
|
|
4247
|
+
// send. Local teardown already happened, so swallow the failure rather
|
|
4248
|
+
// than throw out of a void teardown call.
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
3992
4251
|
}
|
|
3993
4252
|
|
|
3994
4253
|
/**
|
|
@@ -4026,7 +4285,7 @@ export class WebSocketClient {
|
|
|
4026
4285
|
},
|
|
4027
4286
|
};
|
|
4028
4287
|
|
|
4029
|
-
this.
|
|
4288
|
+
this.sendFrame(request);
|
|
4030
4289
|
return stream;
|
|
4031
4290
|
}
|
|
4032
4291
|
|
|
@@ -4052,7 +4311,7 @@ export class WebSocketClient {
|
|
|
4052
4311
|
resolve: () => resolve(),
|
|
4053
4312
|
reject: (err) => reject(err),
|
|
4054
4313
|
};
|
|
4055
|
-
this.
|
|
4314
|
+
this.sendFrame(request);
|
|
4056
4315
|
});
|
|
4057
4316
|
}
|
|
4058
4317
|
|
|
@@ -4079,7 +4338,27 @@ export class WebSocketClient {
|
|
|
4079
4338
|
},
|
|
4080
4339
|
};
|
|
4081
4340
|
|
|
4082
|
-
this.
|
|
4341
|
+
this.sendFrame(request);
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
/**
|
|
4345
|
+
* Cancel an in-flight streaming chat. Fire-and-forget: tells the server to
|
|
4346
|
+
* stop generating tokens for the given chat.
|
|
4347
|
+
*/
|
|
4348
|
+
async cancelChat(chatId: string): Promise<void> {
|
|
4349
|
+
await this.ensureConnected();
|
|
4350
|
+
|
|
4351
|
+
// Attach a unique messageId (same generator as unsubscribe). Any Success ack
|
|
4352
|
+
// from the server then carries a correlation id: it has no pending request to
|
|
4353
|
+
// match, so it is ignored — and because the id is present, the dispatcher's
|
|
4354
|
+
// single-pending fallback can't misroute the ack to an unrelated request.
|
|
4355
|
+
const request = {
|
|
4356
|
+
type: "CancelChat",
|
|
4357
|
+
messageId: this.genMessageId(),
|
|
4358
|
+
payload: { chat_id: chatId },
|
|
4359
|
+
};
|
|
4360
|
+
|
|
4361
|
+
this.sendFrame(request);
|
|
4083
4362
|
}
|
|
4084
4363
|
|
|
4085
4364
|
/**
|