@ekodb/ekodb-client 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +265 -17
- package/dist/client.js +447 -77
- package/dist/client.test.js +246 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/websocket.test.d.ts +6 -0
- package/dist/websocket.test.js +407 -0
- package/package.json +8 -7
- package/src/client.test.ts +341 -1
- package/src/client.ts +683 -86
- package/src/index.ts +14 -0
- package/src/websocket.test.ts +575 -0
- package/tsconfig.json +3 -1
- package/vitest.config.ts +7 -0
package/dist/client.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.WebSocketClient = exports.EkoDBClient = exports.MergeStrategy = exports.RateLimitError = exports.SerializationFormat = void 0;
|
|
39
|
+
exports.WebSocketClient = exports.EventStream = exports.EkoDBClient = exports.MergeStrategy = exports.RateLimitError = exports.SerializationFormat = void 0;
|
|
40
40
|
const msgpack_1 = require("@msgpack/msgpack");
|
|
41
41
|
const query_builder_1 = require("./query-builder");
|
|
42
42
|
const schema_1 = require("./schema");
|
|
@@ -66,6 +66,7 @@ var MergeStrategy;
|
|
|
66
66
|
MergeStrategy["Chronological"] = "Chronological";
|
|
67
67
|
MergeStrategy["Summarized"] = "Summarized";
|
|
68
68
|
MergeStrategy["LatestOnly"] = "LatestOnly";
|
|
69
|
+
MergeStrategy["Interleaved"] = "Interleaved";
|
|
69
70
|
})(MergeStrategy || (exports.MergeStrategy = MergeStrategy = {}));
|
|
70
71
|
class EkoDBClient {
|
|
71
72
|
constructor(config, apiKey) {
|
|
@@ -77,7 +78,6 @@ class EkoDBClient {
|
|
|
77
78
|
this.apiKey = apiKey;
|
|
78
79
|
this.shouldRetry = true;
|
|
79
80
|
this.maxRetries = 3;
|
|
80
|
-
this.timeout = 30000;
|
|
81
81
|
this.format = SerializationFormat.MessagePack; // Default to MessagePack for 2-3x performance
|
|
82
82
|
}
|
|
83
83
|
else {
|
|
@@ -85,7 +85,6 @@ class EkoDBClient {
|
|
|
85
85
|
this.apiKey = config.apiKey;
|
|
86
86
|
this.shouldRetry = config.shouldRetry ?? true;
|
|
87
87
|
this.maxRetries = config.maxRetries ?? 3;
|
|
88
|
-
this.timeout = config.timeout ?? 30000;
|
|
89
88
|
this.format = config.format ?? SerializationFormat.MessagePack; // Default to MessagePack for 2-3x performance
|
|
90
89
|
}
|
|
91
90
|
}
|
|
@@ -135,6 +134,20 @@ class EkoDBClient {
|
|
|
135
134
|
const result = (await response.json());
|
|
136
135
|
this.token = result.token;
|
|
137
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Get the current authentication token.
|
|
139
|
+
* Returns null if not yet authenticated. Call refreshToken() first.
|
|
140
|
+
*/
|
|
141
|
+
getToken() {
|
|
142
|
+
return this.token;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Clear the cached authentication token.
|
|
146
|
+
* The next request will trigger a fresh token exchange.
|
|
147
|
+
*/
|
|
148
|
+
clearTokenCache() {
|
|
149
|
+
this.token = null;
|
|
150
|
+
}
|
|
138
151
|
/**
|
|
139
152
|
* Extract rate limit information from response headers
|
|
140
153
|
*/
|
|
@@ -325,6 +338,26 @@ class EkoDBClient {
|
|
|
325
338
|
async findById(collection, id) {
|
|
326
339
|
return this.makeRequest("GET", `/api/find/${collection}/${id}`);
|
|
327
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Find a document by ID with field projection
|
|
343
|
+
* @param collection - Collection name
|
|
344
|
+
* @param id - Document ID
|
|
345
|
+
* @param selectFields - Fields to include in the result
|
|
346
|
+
* @param excludeFields - Fields to exclude from the result
|
|
347
|
+
*/
|
|
348
|
+
async findByIdWithProjection(collection, id, selectFields, excludeFields) {
|
|
349
|
+
const params = new URLSearchParams();
|
|
350
|
+
if (selectFields?.length) {
|
|
351
|
+
params.append("select_fields", selectFields.join(","));
|
|
352
|
+
}
|
|
353
|
+
if (excludeFields?.length) {
|
|
354
|
+
params.append("exclude_fields", excludeFields.join(","));
|
|
355
|
+
}
|
|
356
|
+
const url = params.toString()
|
|
357
|
+
? `/api/find/${collection}/${id}?${params.toString()}`
|
|
358
|
+
: `/api/find/${collection}/${id}`;
|
|
359
|
+
return this.makeRequest("GET", url);
|
|
360
|
+
}
|
|
328
361
|
/**
|
|
329
362
|
* Update a document
|
|
330
363
|
* @param collection - Collection name
|
|
@@ -345,6 +378,40 @@ class EkoDBClient {
|
|
|
345
378
|
: `/api/update/${collection}/${id}`;
|
|
346
379
|
return this.makeRequest("PUT", url, record);
|
|
347
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* Apply an atomic field action to a single field of a record.
|
|
383
|
+
*
|
|
384
|
+
* Use this instead of `update()` for safe concurrent modifications like
|
|
385
|
+
* incrementing counters, pushing to arrays, or arithmetic operations.
|
|
386
|
+
*
|
|
387
|
+
* @param collection - Collection name
|
|
388
|
+
* @param id - Record ID
|
|
389
|
+
* @param action - The atomic action: increment, decrement, multiply, divide, modulo,
|
|
390
|
+
* push, pop, shift, unshift, remove, append, clear
|
|
391
|
+
* @param field - The field name to apply the action to
|
|
392
|
+
* @param value - The value for the action (omit for pop/shift/clear)
|
|
393
|
+
*/
|
|
394
|
+
async updateWithAction(collection, id, action, field, value) {
|
|
395
|
+
const url = `/api/update/${collection}/${id}/action/${action}`;
|
|
396
|
+
return this.makeRequest("PUT", url, {
|
|
397
|
+
field,
|
|
398
|
+
value: value ?? null,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Apply a sequence of atomic field actions to a record in a single request.
|
|
403
|
+
*
|
|
404
|
+
* All actions are applied atomically — the record is fetched once, all actions
|
|
405
|
+
* run in order, and the result is persisted in a single update.
|
|
406
|
+
*
|
|
407
|
+
* @param collection - Collection name
|
|
408
|
+
* @param id - Record ID
|
|
409
|
+
* @param actions - Array of [action, field, value] tuples
|
|
410
|
+
*/
|
|
411
|
+
async updateWithActionSequence(collection, id, actions) {
|
|
412
|
+
const url = `/api/update/sequence/${collection}/${id}`;
|
|
413
|
+
return this.makeRequest("PUT", url, actions);
|
|
414
|
+
}
|
|
348
415
|
/**
|
|
349
416
|
* Delete a document
|
|
350
417
|
* @param collection - Collection name
|
|
@@ -771,6 +838,36 @@ class EkoDBClient {
|
|
|
771
838
|
// Ensure all parameters from SearchQuery are sent to server
|
|
772
839
|
return this.makeRequest("POST", `/api/search/${collection}`, query, 0, true);
|
|
773
840
|
}
|
|
841
|
+
/**
|
|
842
|
+
* Get distinct (unique) values for a field across all records in a collection.
|
|
843
|
+
*
|
|
844
|
+
* Results are deduplicated and sorted alphabetically. Supports an optional filter
|
|
845
|
+
* to restrict which records are examined.
|
|
846
|
+
*
|
|
847
|
+
* @param collection - Collection name
|
|
848
|
+
* @param field - Field to get distinct values for
|
|
849
|
+
* @param options - Optional filter and bypass flags
|
|
850
|
+
*
|
|
851
|
+
* @example
|
|
852
|
+
* // All distinct statuses
|
|
853
|
+
* const resp = await client.distinctValues("orders", "status");
|
|
854
|
+
* console.log(resp.values); // ["active", "cancelled", "shipped"]
|
|
855
|
+
*
|
|
856
|
+
* // Only statuses for US orders
|
|
857
|
+
* const resp = await client.distinctValues("orders", "status", {
|
|
858
|
+
* filter: { type: "Condition", content: { field: "region", operator: "Eq", value: "us" } }
|
|
859
|
+
* });
|
|
860
|
+
*/
|
|
861
|
+
async distinctValues(collection, field, options = {}) {
|
|
862
|
+
const body = {};
|
|
863
|
+
if (options.filter !== undefined)
|
|
864
|
+
body.filter = options.filter;
|
|
865
|
+
if (options.bypassRipple !== undefined)
|
|
866
|
+
body.bypass_ripple = options.bypassRipple;
|
|
867
|
+
if (options.bypassCache !== undefined)
|
|
868
|
+
body.bypass_cache = options.bypassCache;
|
|
869
|
+
return this.makeRequest("POST", `/api/distinct/${collection}/${field}`, body, 0, true);
|
|
870
|
+
}
|
|
774
871
|
/**
|
|
775
872
|
* Health check - verify the ekoDB server is responding
|
|
776
873
|
*/
|
|
@@ -790,6 +887,25 @@ class EkoDBClient {
|
|
|
790
887
|
async createChatSession(request) {
|
|
791
888
|
return this.makeRequest("POST", "/api/chat", request, 0, true);
|
|
792
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* Stateless raw LLM completion — no session, no history, no RAG.
|
|
892
|
+
*
|
|
893
|
+
* Sends a system prompt and user message directly to the LLM via ekoDB
|
|
894
|
+
* and returns the raw text response without any context injection or
|
|
895
|
+
* conversation management. Use this for structured-output tasks such as
|
|
896
|
+
* planning where the response must be parsed programmatically.
|
|
897
|
+
*
|
|
898
|
+
* @example
|
|
899
|
+
* const resp = await client.rawCompletion({
|
|
900
|
+
* system_prompt: "You are a helpful assistant.",
|
|
901
|
+
* message: "Summarize this in JSON.",
|
|
902
|
+
* max_tokens: 2048,
|
|
903
|
+
* });
|
|
904
|
+
* console.log(resp.content);
|
|
905
|
+
*/
|
|
906
|
+
async rawCompletion(request) {
|
|
907
|
+
return this.makeRequest("POST", "/api/chat/complete", request, 0, true);
|
|
908
|
+
}
|
|
793
909
|
/**
|
|
794
910
|
* Send a message in an existing chat session
|
|
795
911
|
*/
|
|
@@ -889,6 +1005,14 @@ class EkoDBClient {
|
|
|
889
1005
|
async getChatModels() {
|
|
890
1006
|
return this.makeRequest("GET", "/api/chat_models", undefined, 0, true);
|
|
891
1007
|
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Get all built-in server-side chat tool definitions.
|
|
1010
|
+
* Returns a list of tool objects with name, description, and parameters fields.
|
|
1011
|
+
* Used by planning agents to discover available tools dynamically.
|
|
1012
|
+
*/
|
|
1013
|
+
async getChatTools() {
|
|
1014
|
+
return this.makeRequest("GET", "/api/chat/tools", undefined, 0, true);
|
|
1015
|
+
}
|
|
892
1016
|
/**
|
|
893
1017
|
* Get available models for a specific provider
|
|
894
1018
|
* @param provider - Provider name (e.g., "openai", "anthropic", "perplexity")
|
|
@@ -1026,13 +1150,7 @@ class EkoDBClient {
|
|
|
1026
1150
|
}
|
|
1027
1151
|
// ========== RAG Helper Methods ==========
|
|
1028
1152
|
/**
|
|
1029
|
-
* Generate embeddings for
|
|
1030
|
-
*
|
|
1031
|
-
* This helper simplifies embedding generation by:
|
|
1032
|
-
* 1. Creating a temporary collection with the text
|
|
1033
|
-
* 2. Running a Script with FindAll + Embed Functions
|
|
1034
|
-
* 3. Extracting and returning the embedding vector
|
|
1035
|
-
* 4. Cleaning up temporary resources
|
|
1153
|
+
* Generate embeddings for a single text
|
|
1036
1154
|
*
|
|
1037
1155
|
* @param text - The text to generate embeddings for
|
|
1038
1156
|
* @param model - The embedding model to use (e.g., "text-embedding-3-small")
|
|
@@ -1048,52 +1166,28 @@ class EkoDBClient {
|
|
|
1048
1166
|
* ```
|
|
1049
1167
|
*/
|
|
1050
1168
|
async embed(text, model) {
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
await this.insert(tempCollection, { text }, undefined);
|
|
1055
|
-
// Create Script with FindAll + Embed Functions
|
|
1056
|
-
const tempLabel = `embed_script_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
1057
|
-
const script = {
|
|
1058
|
-
label: tempLabel,
|
|
1059
|
-
name: "Generate Embedding",
|
|
1060
|
-
description: "Temporary script for embedding generation",
|
|
1061
|
-
version: "1.0",
|
|
1062
|
-
parameters: {},
|
|
1063
|
-
functions: [
|
|
1064
|
-
{
|
|
1065
|
-
type: "FindAll",
|
|
1066
|
-
collection: tempCollection,
|
|
1067
|
-
},
|
|
1068
|
-
{
|
|
1069
|
-
type: "Embed",
|
|
1070
|
-
input_field: "text",
|
|
1071
|
-
output_field: "embedding",
|
|
1072
|
-
model: model,
|
|
1073
|
-
},
|
|
1074
|
-
],
|
|
1075
|
-
tags: [],
|
|
1076
|
-
};
|
|
1077
|
-
// Save and execute the script
|
|
1078
|
-
const scriptId = await this.saveScript(script);
|
|
1079
|
-
const result = await this.callScript(scriptId, undefined);
|
|
1080
|
-
// Clean up
|
|
1081
|
-
await this.deleteScript(scriptId).catch(() => { });
|
|
1082
|
-
await this.deleteCollection(tempCollection).catch(() => { });
|
|
1083
|
-
// Extract embedding from result
|
|
1084
|
-
if (result.records && result.records.length > 0) {
|
|
1085
|
-
const record = result.records[0];
|
|
1086
|
-
if (record.embedding && Array.isArray(record.embedding)) {
|
|
1087
|
-
return record.embedding;
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
throw new Error("Failed to extract embedding from result");
|
|
1091
|
-
}
|
|
1092
|
-
catch (error) {
|
|
1093
|
-
// Ensure cleanup even on error
|
|
1094
|
-
await this.deleteCollection(tempCollection).catch(() => { });
|
|
1095
|
-
throw error;
|
|
1169
|
+
const response = await this.embedRequest({ text, model });
|
|
1170
|
+
if (response.embeddings.length === 0) {
|
|
1171
|
+
throw new Error("No embedding returned");
|
|
1096
1172
|
}
|
|
1173
|
+
return response.embeddings[0];
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Generate embeddings for multiple texts in a single batch request
|
|
1177
|
+
*
|
|
1178
|
+
* @param texts - Array of texts to generate embeddings for
|
|
1179
|
+
* @param model - The embedding model to use
|
|
1180
|
+
* @returns Array of embedding vectors
|
|
1181
|
+
*/
|
|
1182
|
+
async embedBatch(texts, model) {
|
|
1183
|
+
const response = await this.embedRequest({ texts, model });
|
|
1184
|
+
return response.embeddings;
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Internal: make embed API request
|
|
1188
|
+
*/
|
|
1189
|
+
async embedRequest(request) {
|
|
1190
|
+
return this.makeRequest("POST", "/api/embed", request, 0, true);
|
|
1097
1191
|
}
|
|
1098
1192
|
/**
|
|
1099
1193
|
* Perform text search without embeddings
|
|
@@ -1179,22 +1273,64 @@ class EkoDBClient {
|
|
|
1179
1273
|
}
|
|
1180
1274
|
}
|
|
1181
1275
|
exports.EkoDBClient = EkoDBClient;
|
|
1276
|
+
/** EventEmitter-like interface for subscriptions and chat streams. */
|
|
1277
|
+
class EventStream {
|
|
1278
|
+
constructor() {
|
|
1279
|
+
this.listeners = new Map();
|
|
1280
|
+
this._closed = false;
|
|
1281
|
+
}
|
|
1282
|
+
on(event, listener) {
|
|
1283
|
+
if (!this.listeners.has(event)) {
|
|
1284
|
+
this.listeners.set(event, []);
|
|
1285
|
+
}
|
|
1286
|
+
this.listeners.get(event).push(listener);
|
|
1287
|
+
return this;
|
|
1288
|
+
}
|
|
1289
|
+
/** @internal */
|
|
1290
|
+
emit(event, data) {
|
|
1291
|
+
const handlers = this.listeners.get(event);
|
|
1292
|
+
if (handlers) {
|
|
1293
|
+
for (const handler of handlers) {
|
|
1294
|
+
handler(data);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
get closed() {
|
|
1299
|
+
return this._closed;
|
|
1300
|
+
}
|
|
1301
|
+
/** @internal */
|
|
1302
|
+
close() {
|
|
1303
|
+
this._closed = true;
|
|
1304
|
+
this.emit("close");
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
exports.EventStream = EventStream;
|
|
1182
1308
|
/**
|
|
1183
|
-
* WebSocket client for real-time queries
|
|
1309
|
+
* WebSocket client for real-time queries, subscriptions, and chat streaming.
|
|
1184
1310
|
*/
|
|
1185
1311
|
class WebSocketClient {
|
|
1186
1312
|
constructor(wsURL, token) {
|
|
1187
1313
|
this.ws = null;
|
|
1314
|
+
this.dispatcherRunning = false;
|
|
1315
|
+
// Dispatcher state
|
|
1316
|
+
this.pendingRequests = new Map();
|
|
1317
|
+
this.subscriptions = new Map();
|
|
1318
|
+
this.chatStreams = new Map();
|
|
1319
|
+
this.registerToolsAck = null;
|
|
1320
|
+
this.messageCounter = 0;
|
|
1188
1321
|
this.wsURL = wsURL;
|
|
1189
1322
|
this.token = token;
|
|
1190
1323
|
}
|
|
1324
|
+
genMessageId() {
|
|
1325
|
+
const counter = this.messageCounter++;
|
|
1326
|
+
return `${Date.now()}-${counter}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1327
|
+
}
|
|
1191
1328
|
/**
|
|
1192
|
-
* Connect
|
|
1329
|
+
* Connect and start the dispatcher.
|
|
1193
1330
|
*/
|
|
1194
|
-
async
|
|
1195
|
-
if (this.ws)
|
|
1331
|
+
async ensureConnected() {
|
|
1332
|
+
if (this.ws && this.dispatcherRunning)
|
|
1196
1333
|
return;
|
|
1197
|
-
// Dynamic import for Node.js WebSocket
|
|
1198
1334
|
const WebSocket = (await Promise.resolve().then(() => __importStar(require("ws")))).default;
|
|
1199
1335
|
let url = this.wsURL;
|
|
1200
1336
|
if (!url.endsWith("/api/ws")) {
|
|
@@ -1205,43 +1341,277 @@ class WebSocketClient {
|
|
|
1205
1341
|
Authorization: `Bearer ${this.token}`,
|
|
1206
1342
|
},
|
|
1207
1343
|
});
|
|
1208
|
-
|
|
1344
|
+
await new Promise((resolve, reject) => {
|
|
1209
1345
|
this.ws.on("open", () => resolve());
|
|
1210
1346
|
this.ws.on("error", (err) => reject(err));
|
|
1211
1347
|
});
|
|
1348
|
+
this.spawnDispatcher();
|
|
1349
|
+
}
|
|
1350
|
+
spawnDispatcher() {
|
|
1351
|
+
if (this.dispatcherRunning)
|
|
1352
|
+
return;
|
|
1353
|
+
this.dispatcherRunning = true;
|
|
1354
|
+
this.ws.on("message", (data) => {
|
|
1355
|
+
try {
|
|
1356
|
+
const msg = JSON.parse(data.toString());
|
|
1357
|
+
this.routeMessage(msg);
|
|
1358
|
+
}
|
|
1359
|
+
catch {
|
|
1360
|
+
// Ignore malformed messages
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
this.ws.on("close", () => {
|
|
1364
|
+
this.dispatcherRunning = false;
|
|
1365
|
+
// Notify all pending requests
|
|
1366
|
+
for (const [, pending] of this.pendingRequests) {
|
|
1367
|
+
pending.reject(new Error("WebSocket connection closed"));
|
|
1368
|
+
}
|
|
1369
|
+
this.pendingRequests.clear();
|
|
1370
|
+
// Close all chat streams
|
|
1371
|
+
for (const [, stream] of this.chatStreams) {
|
|
1372
|
+
stream.emit("event", { type: "error", error: "Connection closed" });
|
|
1373
|
+
stream.close();
|
|
1374
|
+
}
|
|
1375
|
+
this.chatStreams.clear();
|
|
1376
|
+
// Close all subscriptions
|
|
1377
|
+
for (const [, stream] of this.subscriptions) {
|
|
1378
|
+
stream.close();
|
|
1379
|
+
}
|
|
1380
|
+
this.subscriptions.clear();
|
|
1381
|
+
this.ws = null;
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
routeMessage(msg) {
|
|
1385
|
+
switch (msg.type) {
|
|
1386
|
+
case "Success":
|
|
1387
|
+
case "Error": {
|
|
1388
|
+
const messageId = msg.payload?.message_id || msg.payload?.messageId;
|
|
1389
|
+
if (messageId && this.pendingRequests.has(messageId)) {
|
|
1390
|
+
const pending = this.pendingRequests.get(messageId);
|
|
1391
|
+
this.pendingRequests.delete(messageId);
|
|
1392
|
+
if (msg.type === "Error") {
|
|
1393
|
+
pending.reject(new Error(msg.message || "Unknown error"));
|
|
1394
|
+
}
|
|
1395
|
+
else {
|
|
1396
|
+
pending.resolve(msg.payload);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
else if (this.registerToolsAck) {
|
|
1400
|
+
const ack = this.registerToolsAck;
|
|
1401
|
+
this.registerToolsAck = null;
|
|
1402
|
+
if (msg.type === "Error") {
|
|
1403
|
+
ack.reject(new Error(msg.message || "Tool registration failed"));
|
|
1404
|
+
}
|
|
1405
|
+
else {
|
|
1406
|
+
ack.resolve(msg.payload);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
break;
|
|
1410
|
+
}
|
|
1411
|
+
case "MutationNotification": {
|
|
1412
|
+
const payload = msg.payload;
|
|
1413
|
+
const notification = {
|
|
1414
|
+
collection: payload.collection,
|
|
1415
|
+
event: payload.event,
|
|
1416
|
+
recordIds: payload.record_ids || payload.recordIds || [],
|
|
1417
|
+
records: payload.records,
|
|
1418
|
+
timestamp: payload.timestamp,
|
|
1419
|
+
};
|
|
1420
|
+
for (const [collection, stream] of this.subscriptions) {
|
|
1421
|
+
if (collection === notification.collection) {
|
|
1422
|
+
stream.emit("mutation", notification);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
break;
|
|
1426
|
+
}
|
|
1427
|
+
case "ChatStreamChunk": {
|
|
1428
|
+
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
1429
|
+
const stream = this.chatStreams.get(chatId);
|
|
1430
|
+
if (stream) {
|
|
1431
|
+
stream.emit("event", {
|
|
1432
|
+
type: "chunk",
|
|
1433
|
+
content: msg.payload.content,
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
break;
|
|
1437
|
+
}
|
|
1438
|
+
case "ChatStreamEnd": {
|
|
1439
|
+
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
1440
|
+
const stream = this.chatStreams.get(chatId);
|
|
1441
|
+
if (stream) {
|
|
1442
|
+
stream.emit("event", {
|
|
1443
|
+
type: "end",
|
|
1444
|
+
messageId: msg.payload.message_id || msg.payload.messageId || "",
|
|
1445
|
+
tokenUsage: msg.payload.token_usage || msg.payload.tokenUsage,
|
|
1446
|
+
toolCallHistory: msg.payload.tool_call_history || msg.payload.toolCallHistory,
|
|
1447
|
+
executionTimeMs: msg.payload.execution_time_ms || msg.payload.executionTimeMs || 0,
|
|
1448
|
+
});
|
|
1449
|
+
this.chatStreams.delete(chatId);
|
|
1450
|
+
stream.close();
|
|
1451
|
+
}
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
case "ChatStreamError": {
|
|
1455
|
+
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
1456
|
+
const stream = this.chatStreams.get(chatId);
|
|
1457
|
+
if (stream) {
|
|
1458
|
+
stream.emit("event", {
|
|
1459
|
+
type: "error",
|
|
1460
|
+
error: msg.payload.error || msg.payload.message || "Unknown error",
|
|
1461
|
+
});
|
|
1462
|
+
this.chatStreams.delete(chatId);
|
|
1463
|
+
stream.close();
|
|
1464
|
+
}
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
case "ClientToolCall": {
|
|
1468
|
+
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
1469
|
+
const stream = this.chatStreams.get(chatId);
|
|
1470
|
+
if (stream) {
|
|
1471
|
+
stream.emit("event", {
|
|
1472
|
+
type: "toolCall",
|
|
1473
|
+
chatId,
|
|
1474
|
+
callId: msg.payload.call_id || msg.payload.callId,
|
|
1475
|
+
toolName: msg.payload.tool_name || msg.payload.toolName,
|
|
1476
|
+
arguments: msg.payload.arguments,
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
break;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
async sendRequest(request) {
|
|
1484
|
+
await this.ensureConnected();
|
|
1485
|
+
const messageId = request.messageId || request.message_id;
|
|
1486
|
+
return new Promise((resolve, reject) => {
|
|
1487
|
+
this.pendingRequests.set(messageId, { resolve, reject });
|
|
1488
|
+
try {
|
|
1489
|
+
this.ws.send(JSON.stringify(request));
|
|
1490
|
+
}
|
|
1491
|
+
catch (err) {
|
|
1492
|
+
this.pendingRequests.delete(messageId);
|
|
1493
|
+
reject(err);
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1212
1496
|
}
|
|
1213
1497
|
/**
|
|
1214
|
-
* Find all records in a collection via WebSocket
|
|
1498
|
+
* Find all records in a collection via WebSocket.
|
|
1215
1499
|
*/
|
|
1216
1500
|
async findAll(collection) {
|
|
1217
|
-
|
|
1218
|
-
const
|
|
1219
|
-
const request = {
|
|
1501
|
+
const messageId = this.genMessageId();
|
|
1502
|
+
const payload = await this.sendRequest({
|
|
1220
1503
|
type: "FindAll",
|
|
1221
1504
|
messageId,
|
|
1222
1505
|
payload: { collection },
|
|
1506
|
+
});
|
|
1507
|
+
return payload?.data || [];
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Subscribe to mutation notifications on a collection.
|
|
1511
|
+
* Returns an EventStream that emits "mutation" events.
|
|
1512
|
+
*/
|
|
1513
|
+
async subscribe(collection, options) {
|
|
1514
|
+
await this.ensureConnected();
|
|
1515
|
+
if (this.subscriptions.has(collection)) {
|
|
1516
|
+
throw new Error(`Already subscribed to collection "${collection}"`);
|
|
1517
|
+
}
|
|
1518
|
+
const messageId = this.genMessageId();
|
|
1519
|
+
const stream = new EventStream();
|
|
1520
|
+
this.subscriptions.set(collection, stream);
|
|
1521
|
+
const request = {
|
|
1522
|
+
type: "Subscribe",
|
|
1523
|
+
messageId,
|
|
1524
|
+
payload: {
|
|
1525
|
+
collection,
|
|
1526
|
+
...(options?.filterField && { filter_field: options.filterField }),
|
|
1527
|
+
...(options?.filterValue && { filter_value: options.filterValue }),
|
|
1528
|
+
},
|
|
1223
1529
|
};
|
|
1224
|
-
|
|
1530
|
+
// Send subscribe request and wait for ack
|
|
1531
|
+
try {
|
|
1532
|
+
await this.sendRequest(request);
|
|
1533
|
+
}
|
|
1534
|
+
catch (err) {
|
|
1535
|
+
this.subscriptions.delete(collection);
|
|
1536
|
+
throw err;
|
|
1537
|
+
}
|
|
1538
|
+
return stream;
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Send a chat message and receive a streaming response.
|
|
1542
|
+
* Returns an EventStream that emits "event" with ChatStreamEvent objects.
|
|
1543
|
+
*/
|
|
1544
|
+
async chatSend(chatId, message, options) {
|
|
1545
|
+
await this.ensureConnected();
|
|
1546
|
+
if (this.chatStreams.has(chatId)) {
|
|
1547
|
+
throw new Error(`Chat stream already active for chatId "${chatId}"`);
|
|
1548
|
+
}
|
|
1549
|
+
const stream = new EventStream();
|
|
1550
|
+
this.chatStreams.set(chatId, stream);
|
|
1551
|
+
const request = {
|
|
1552
|
+
type: "ChatSend",
|
|
1553
|
+
payload: {
|
|
1554
|
+
chat_id: chatId,
|
|
1555
|
+
message,
|
|
1556
|
+
...(options?.bypassRipple != null && {
|
|
1557
|
+
bypass_ripple: options.bypassRipple,
|
|
1558
|
+
}),
|
|
1559
|
+
...(options?.clientTools && { client_tools: options.clientTools }),
|
|
1560
|
+
...(options?.maxIterations != null && {
|
|
1561
|
+
max_iterations: options.maxIterations,
|
|
1562
|
+
}),
|
|
1563
|
+
...(options?.confirmTools && { confirm_tools: options.confirmTools }),
|
|
1564
|
+
...(options?.excludeTools && { exclude_tools: options.excludeTools }),
|
|
1565
|
+
},
|
|
1566
|
+
};
|
|
1567
|
+
this.ws.send(JSON.stringify(request));
|
|
1568
|
+
return stream;
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Register client-side tools for a chat session.
|
|
1572
|
+
*/
|
|
1573
|
+
async registerClientTools(chatId, tools) {
|
|
1574
|
+
await this.ensureConnected();
|
|
1575
|
+
const request = {
|
|
1576
|
+
type: "RegisterClientTools",
|
|
1577
|
+
payload: {
|
|
1578
|
+
chat_id: chatId,
|
|
1579
|
+
tools,
|
|
1580
|
+
},
|
|
1581
|
+
};
|
|
1582
|
+
await new Promise((resolve, reject) => {
|
|
1583
|
+
this.registerToolsAck = {
|
|
1584
|
+
resolve: () => resolve(),
|
|
1585
|
+
reject: (err) => reject(err),
|
|
1586
|
+
};
|
|
1225
1587
|
this.ws.send(JSON.stringify(request));
|
|
1226
|
-
this.ws.once("message", (data) => {
|
|
1227
|
-
const response = JSON.parse(data.toString());
|
|
1228
|
-
if (response.type === "Error") {
|
|
1229
|
-
reject(new Error(response.message));
|
|
1230
|
-
}
|
|
1231
|
-
else {
|
|
1232
|
-
resolve(response.payload?.data || []);
|
|
1233
|
-
}
|
|
1234
|
-
});
|
|
1235
|
-
this.ws.once("error", reject);
|
|
1236
1588
|
});
|
|
1237
1589
|
}
|
|
1238
1590
|
/**
|
|
1239
|
-
*
|
|
1591
|
+
* Send a tool result back to the server during a chat stream.
|
|
1592
|
+
*/
|
|
1593
|
+
async sendToolResult(chatId, callId, success, result, error) {
|
|
1594
|
+
await this.ensureConnected();
|
|
1595
|
+
const request = {
|
|
1596
|
+
type: "ClientToolResult",
|
|
1597
|
+
payload: {
|
|
1598
|
+
chat_id: chatId,
|
|
1599
|
+
call_id: callId,
|
|
1600
|
+
success,
|
|
1601
|
+
...(result !== undefined && { result }),
|
|
1602
|
+
...(error !== undefined && { error }),
|
|
1603
|
+
},
|
|
1604
|
+
};
|
|
1605
|
+
this.ws.send(JSON.stringify(request));
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Close the WebSocket connection.
|
|
1240
1609
|
*/
|
|
1241
1610
|
close() {
|
|
1242
1611
|
if (this.ws) {
|
|
1243
1612
|
this.ws.close();
|
|
1244
1613
|
this.ws = null;
|
|
1614
|
+
this.dispatcherRunning = false;
|
|
1245
1615
|
}
|
|
1246
1616
|
}
|
|
1247
1617
|
}
|