typesense
Version:
Javascript Library for Typesense
777 lines • 44.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var axios_1 = tslib_1.__importDefault(require("axios"));
var http_1 = require("http");
var https_1 = require("https");
var Errors_1 = require("./Errors");
var TypesenseError_1 = tslib_1.__importDefault(require("./Errors/TypesenseError"));
var Utils_1 = require("./Utils");
var APIKEYHEADERNAME = "X-TYPESENSE-API-KEY";
var HEALTHY = true;
var UNHEALTHY = false;
var isNodeJSEnvironment = typeof process !== "undefined" &&
process.versions != null &&
process.versions.node != null &&
typeof window === "undefined";
var ApiCall = /** @class */ (function () {
function ApiCall(configuration) {
this.configuration = configuration;
this.apiKey = this.configuration.apiKey;
this.nodes =
this.configuration.nodes == null
? this.configuration.nodes
: JSON.parse(JSON.stringify(this.configuration.nodes)); // Make a copy, since we'll be adding additional metadata to the nodes
this.nearestNode =
this.configuration.nearestNode == null
? this.configuration.nearestNode
: JSON.parse(JSON.stringify(this.configuration.nearestNode));
this.connectionTimeoutSeconds = this.configuration.connectionTimeoutSeconds;
this.healthcheckIntervalSeconds =
this.configuration.healthcheckIntervalSeconds;
this.numRetriesPerRequest = this.configuration.numRetries;
this.retryIntervalSeconds = this.configuration.retryIntervalSeconds;
this.sendApiKeyAsQueryParam = this.configuration.sendApiKeyAsQueryParam;
this.additionalUserHeaders = this.configuration.additionalHeaders;
this.logger = this.configuration.logger;
this.initializeMetadataForNodes();
this.currentNodeIndex = -1;
}
ApiCall.prototype.get = function (endpoint_1) {
return tslib_1.__awaiter(this, arguments, void 0, function (endpoint, queryParameters, _a) {
if (queryParameters === void 0) { queryParameters = {}; }
var _b = _a === void 0 ? {} : _a, _c = _b.abortSignal, abortSignal = _c === void 0 ? null : _c, _d = _b.responseType, responseType = _d === void 0 ? undefined : _d, _e = _b.streamConfig, streamConfig = _e === void 0 ? undefined : _e, isStreamingRequest = _b.isStreamingRequest;
return tslib_1.__generator(this, function (_f) {
return [2 /*return*/, this.performRequest("get", endpoint, {
queryParameters: queryParameters,
abortSignal: abortSignal,
responseType: responseType,
streamConfig: streamConfig,
isStreamingRequest: isStreamingRequest,
})];
});
});
};
ApiCall.prototype.delete = function (endpoint_1) {
return tslib_1.__awaiter(this, arguments, void 0, function (endpoint, queryParameters) {
if (queryParameters === void 0) { queryParameters = {}; }
return tslib_1.__generator(this, function (_a) {
return [2 /*return*/, this.performRequest("delete", endpoint, {
queryParameters: queryParameters,
isStreamingRequest: false,
})];
});
});
};
ApiCall.prototype.post = function (endpoint_1) {
return tslib_1.__awaiter(this, arguments, void 0, function (endpoint, bodyParameters, queryParameters, additionalHeaders, _a) {
if (bodyParameters === void 0) { bodyParameters = {}; }
if (queryParameters === void 0) { queryParameters = {}; }
if (additionalHeaders === void 0) { additionalHeaders = {}; }
var _b = _a === void 0 ? {} : _a, _c = _b.abortSignal, abortSignal = _c === void 0 ? null : _c, _d = _b.responseType, responseType = _d === void 0 ? undefined : _d, _e = _b.streamConfig, streamConfig = _e === void 0 ? undefined : _e, isStreamingRequest = _b.isStreamingRequest;
return tslib_1.__generator(this, function (_f) {
return [2 /*return*/, this.performRequest("post", endpoint, {
queryParameters: queryParameters,
bodyParameters: bodyParameters,
additionalHeaders: additionalHeaders,
abortSignal: abortSignal,
responseType: responseType,
streamConfig: streamConfig,
isStreamingRequest: isStreamingRequest,
})];
});
});
};
ApiCall.prototype.put = function (endpoint_1) {
return tslib_1.__awaiter(this, arguments, void 0, function (endpoint, bodyParameters, queryParameters) {
if (bodyParameters === void 0) { bodyParameters = {}; }
if (queryParameters === void 0) { queryParameters = {}; }
return tslib_1.__generator(this, function (_a) {
return [2 /*return*/, this.performRequest("put", endpoint, {
queryParameters: queryParameters,
bodyParameters: bodyParameters,
isStreamingRequest: false,
})];
});
});
};
ApiCall.prototype.patch = function (endpoint_1) {
return tslib_1.__awaiter(this, arguments, void 0, function (endpoint, bodyParameters, queryParameters) {
if (bodyParameters === void 0) { bodyParameters = {}; }
if (queryParameters === void 0) { queryParameters = {}; }
return tslib_1.__generator(this, function (_a) {
return [2 /*return*/, this.performRequest("patch", endpoint, {
queryParameters: queryParameters,
bodyParameters: bodyParameters,
isStreamingRequest: false,
})];
});
});
};
ApiCall.prototype.getAdapter = function () {
if (!this.configuration.axiosAdapter)
return undefined;
if (typeof this.configuration.axiosAdapter === "function")
return this.configuration.axiosAdapter;
var isCloudflareWorkers = typeof navigator !== "undefined" &&
navigator.userAgent === "Cloudflare-Workers";
return isCloudflareWorkers
? axios_1.default.getAdapter(this.configuration.axiosAdapter).bind(globalThis)
: axios_1.default.getAdapter(this.configuration.axiosAdapter);
};
ApiCall.prototype.performRequest = function (requestType_1, endpoint_1, _a) {
return tslib_1.__awaiter(this, arguments, void 0, function (requestType, endpoint, _b) {
var requestNumber, lastException, wasAborted, _loop_1, this_1, numTries, state_1;
var _c, _d, _e, _f;
var _g = _b.queryParameters, queryParameters = _g === void 0 ? null : _g, _h = _b.bodyParameters, bodyParameters = _h === void 0 ? null : _h, _j = _b.additionalHeaders, additionalHeaders = _j === void 0 ? {} : _j, _k = _b.abortSignal, abortSignal = _k === void 0 ? null : _k, _l = _b.responseType, responseType = _l === void 0 ? undefined : _l, _m = _b.skipConnectionTimeout, skipConnectionTimeout = _m === void 0 ? false : _m, _o = _b.enableKeepAlive, enableKeepAlive = _o === void 0 ? undefined : _o, _p = _b.streamConfig, streamConfig = _p === void 0 ? undefined : _p, isStreamingRequest = _b.isStreamingRequest;
return tslib_1.__generator(this, function (_q) {
switch (_q.label) {
case 0:
this.configuration.validate();
if (isStreamingRequest) {
this.logger.debug("Request: Performing streaming request to ".concat(endpoint));
// For browser streaming, always use responseType: "stream" and adapter: "fetch"
if (!isNodeJSEnvironment && typeof fetch !== "undefined") {
this.logger.debug("Using fetch adapter for browser streaming");
responseType = "stream";
}
}
requestNumber = Date.now();
wasAborted = false;
this.logger.debug("Request #".concat(requestNumber, ": Performing ").concat(requestType.toUpperCase(), " request: ").concat(endpoint));
_loop_1 = function (numTries) {
var node, abortListener, requestOptions, cancelToken, source_1, response, error_1;
return tslib_1.__generator(this, function (_r) {
switch (_r.label) {
case 0:
node = this_1.getNextNode(requestNumber);
this_1.logger.debug("Request #".concat(requestNumber, ": Attempting ").concat(requestType.toUpperCase(), " request Try #").concat(numTries, " to Node ").concat(node.index));
if (abortSignal && abortSignal.aborted) {
return [2 /*return*/, { value: Promise.reject(new Error("Request aborted by caller.")) }];
}
abortListener = void 0;
_r.label = 1;
case 1:
_r.trys.push([1, 3, 5, 6]);
requestOptions = {
method: requestType,
url: this_1.uriFor(endpoint, node),
headers: Object.assign({}, this_1.defaultHeaders(), additionalHeaders, this_1.additionalUserHeaders),
maxContentLength: Infinity,
maxBodyLength: Infinity,
validateStatus: function (status) {
/* Override default validateStatus, which only considers 2xx a success.
In our case, if the server returns any HTTP code, we will handle it below.
We do this to be able to raise custom errors based on response code.
*/
return status > 0;
},
transformResponse: [
function (data, headers) {
var transformedData = data;
if (headers !== undefined &&
typeof data === "string" &&
headers["content-type"] &&
headers["content-type"].startsWith("application/json")) {
transformedData = JSON.parse(data);
}
return transformedData;
},
],
};
// Use fetch adapter only for streaming requests in browser environments
requestOptions.adapter =
isStreamingRequest && !isNodeJSEnvironment
? "fetch"
: this_1.getAdapter();
if (skipConnectionTimeout !== true) {
requestOptions.timeout = this_1.connectionTimeoutSeconds * 1000;
}
if (queryParameters && Object.keys(queryParameters).length !== 0) {
requestOptions.params = queryParameters;
}
if (this_1.sendApiKeyAsQueryParam) {
requestOptions.params = requestOptions.params || {};
requestOptions.params["x-typesense-api-key"] = this_1.apiKey;
}
if (this_1.configuration.httpAgent) {
this_1.logger.debug("Request #".concat(requestNumber, ": Using custom httpAgent"));
requestOptions.httpAgent = this_1.configuration.httpAgent;
}
else if (enableKeepAlive === true) {
if (!isNodeJSEnvironment) {
this_1.logger.warn("Request #".concat(requestNumber, ": Cannot use custom httpAgent in a browser environment to enable keepAlive"));
}
else {
this_1.logger.debug("Request #".concat(requestNumber, ": Enabling KeepAlive"));
requestOptions.httpAgent = new http_1.Agent({ keepAlive: true });
}
}
if (this_1.configuration.httpsAgent) {
this_1.logger.debug("Request #".concat(requestNumber, ": Using custom httpsAgent"));
requestOptions.httpsAgent = this_1.configuration.httpsAgent;
}
else if (enableKeepAlive === true) {
if (!isNodeJSEnvironment) {
this_1.logger.warn("Request #".concat(requestNumber, ": Cannot use custom httpAgent in a browser environment to enable keepAlive"));
}
else {
this_1.logger.debug("Request #".concat(requestNumber, ": Enabling keepAlive"));
requestOptions.httpsAgent = new https_1.Agent({ keepAlive: true });
}
}
if (this_1.configuration.paramsSerializer) {
this_1.logger.debug("Request #".concat(requestNumber, ": Using custom paramsSerializer"));
requestOptions.paramsSerializer = this_1.configuration.paramsSerializer;
}
if (bodyParameters &&
((typeof bodyParameters === "string" &&
bodyParameters.length !== 0) ||
(typeof bodyParameters === "object" &&
Object.keys(bodyParameters).length !== 0))) {
requestOptions.data = bodyParameters;
}
// Translate from user-provided AbortController to the Axios request cancel mechanism.
if (abortSignal) {
cancelToken = axios_1.default.CancelToken;
source_1 = cancelToken.source();
abortListener = function () {
wasAborted = true;
source_1.cancel();
};
abortSignal.addEventListener("abort", abortListener);
requestOptions.cancelToken = source_1.token;
}
if (isStreamingRequest) {
requestOptions.responseType = "stream";
if (!isNodeJSEnvironment) {
requestOptions.headers = tslib_1.__assign(tslib_1.__assign({}, requestOptions.headers), { Accept: "text/event-stream" });
}
}
else if (responseType) {
requestOptions.responseType = responseType;
}
return [4 /*yield*/, (0, axios_1.default)(requestOptions)];
case 2:
response = _r.sent();
if (response.status >= 1 && response.status <= 499) {
// Treat any status code > 0 and < 500 to be an indication that node is healthy
// We exclude 0 since some clients return 0 when request fails
this_1.setNodeHealthcheck(node, HEALTHY);
}
this_1.logger.debug("Request #".concat(requestNumber, ": Request to Node ").concat(node.index, " was made. Response Code was ").concat(response.status, "."));
if (response.status >= 200 && response.status < 300) {
if (isStreamingRequest) {
return [2 /*return*/, { value: this_1.handleStreamingResponse(response, streamConfig) }];
}
return [2 /*return*/, { value: Promise.resolve(response.data) }];
}
else if (response.status < 500) {
return [2 /*return*/, { value: Promise.reject(this_1.customErrorForResponse(response, (_c = response.data) === null || _c === void 0 ? void 0 : _c.message, requestOptions.data)) }];
}
else {
// Retry all other HTTP errors (HTTPStatus > 500)
// This will get caught by the catch block below
throw this_1.customErrorForResponse(response, (_d = response.data) === null || _d === void 0 ? void 0 : _d.message, requestOptions.data);
}
return [3 /*break*/, 6];
case 3:
error_1 = _r.sent();
// This block handles retries for HTTPStatus > 500 and network layer issues like connection timeouts
if (!wasAborted) {
this_1.setNodeHealthcheck(node, UNHEALTHY);
}
lastException = error_1;
this_1.logger.warn("Request #".concat(requestNumber, ": Request to Node ").concat(node.index, " failed due to \"").concat((_e = error_1 === null || error_1 === void 0 ? void 0 : error_1.code) !== null && _e !== void 0 ? _e : "", " ").concat(error_1.message).concat(error_1.response == null
? ""
: " - " + JSON.stringify((_f = error_1.response) === null || _f === void 0 ? void 0 : _f.data), "\""));
if (wasAborted) {
return [2 /*return*/, { value: Promise.reject(new Error("Request aborted by caller.")) }];
}
if (isStreamingRequest) {
this_1.invokeOnErrorCallback(error_1, streamConfig);
}
if (numTries < this_1.numRetriesPerRequest + 1) {
this_1.logger.warn("Request #".concat(requestNumber, ": Sleeping for ").concat(this_1.retryIntervalSeconds, "s and then retrying request..."));
}
else {
this_1.logger.debug("Request #".concat(requestNumber, ": No retries left. Raising last error"));
return [2 /*return*/, { value: Promise.reject(lastException) }];
}
return [4 /*yield*/, this_1.timer(this_1.retryIntervalSeconds)];
case 4:
_r.sent();
return [3 /*break*/, 6];
case 5:
if (abortSignal && abortListener) {
abortSignal.removeEventListener("abort", abortListener);
}
return [7 /*endfinally*/];
case 6: return [2 /*return*/];
}
});
};
this_1 = this;
numTries = 1;
_q.label = 1;
case 1:
if (!(numTries <= this.numRetriesPerRequest + 1)) return [3 /*break*/, 4];
return [5 /*yield**/, _loop_1(numTries)];
case 2:
state_1 = _q.sent();
if (typeof state_1 === "object")
return [2 /*return*/, state_1.value];
_q.label = 3;
case 3:
numTries++;
return [3 /*break*/, 1];
case 4:
this.logger.debug("Request #".concat(requestNumber, ": No retries left. Raising last error"));
return [2 /*return*/, Promise.reject(lastException)];
}
});
});
};
ApiCall.prototype.processStreamingLine = function (line) {
if (!line.trim() || line === "data: [DONE]") {
return null;
}
// Handle SSE format (data: {...})
if (line.startsWith("data: ")) {
return this.processDataLine(line.slice(6).trim());
}
// Try parsing as JSON if it starts with a brace
if (line.trim().startsWith("{")) {
try {
var jsonData = JSON.parse(line.trim());
if (jsonData && typeof jsonData === "object") {
if (!jsonData.conversation_id) {
jsonData.conversation_id = "unknown";
}
if (!jsonData.message && jsonData.message !== "") {
jsonData.message = "";
}
return jsonData;
}
return {
conversation_id: "unknown",
message: JSON.stringify(jsonData),
};
}
catch (_a) {
return {
conversation_id: "unknown",
message: line.trim(),
};
}
}
return {
conversation_id: "unknown",
message: line.trim(),
};
};
ApiCall.prototype.processDataLine = function (dataContent) {
if (!dataContent) {
return null;
}
if (dataContent.startsWith("{")) {
try {
var jsonData = JSON.parse(dataContent);
// Ensure the required fields exist
if (jsonData && typeof jsonData === "object") {
if (!jsonData.conversation_id) {
jsonData.conversation_id = "unknown";
}
if (!jsonData.message && jsonData.message !== "") {
jsonData.message = "";
}
return jsonData;
}
return {
conversation_id: "unknown",
message: JSON.stringify(jsonData),
};
}
catch (_a) {
// Not valid JSON, use as plain text
return {
conversation_id: "unknown",
message: dataContent,
};
}
}
// For plain text
return {
conversation_id: "unknown",
message: dataContent,
};
};
ApiCall.prototype.handleStreamingResponse = function (response, streamConfig) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
return tslib_1.__generator(this, function (_a) {
this.logger.debug("Handling streaming response. Environment: ".concat(isNodeJSEnvironment ? "Node.js" : "Browser"));
if (isNodeJSEnvironment && response.data) {
return [2 /*return*/, this.handleNodeStreaming(response, streamConfig)];
}
if (!isNodeJSEnvironment) {
return [2 /*return*/, this.handleBrowserStreaming(response, streamConfig)];
}
this.logger.debug("Processing non-streaming response");
this.invokeOnCompleteCallback(response.data, streamConfig);
return [2 /*return*/, Promise.resolve(response.data)];
});
});
};
ApiCall.prototype.handleNodeStreaming = function (response, streamConfig) {
var _this = this;
this.logger.debug("Processing Node.js stream");
return new Promise(function (resolve, reject) {
var stream = response.data;
var allChunks = [];
var buffer = "";
stream.on("data", function (chunk) {
var _a;
try {
var data = chunk.toString();
buffer += data;
var lines = buffer.split("\n");
buffer = (_a = lines.pop()) !== null && _a !== void 0 ? _a : "";
_this.processStreamLines(lines, allChunks, streamConfig);
}
catch (error) {
reject(error);
}
});
stream.on("end", function () {
if (buffer.trim().length > 0) {
var lines = buffer.split("\n");
_this.processStreamLines(lines, allChunks, streamConfig);
}
_this.finalizeStreamResult(allChunks, resolve, response, streamConfig);
});
stream.on("error", function (error) {
_this.logger.error("Stream error: ".concat(error));
_this.invokeOnErrorCallback(error, streamConfig);
reject(error);
});
});
};
ApiCall.prototype.handleBrowserStreaming = function (response, streamConfig) {
var _this = this;
this.logger.debug("Processing browser stream");
return new Promise(function (resolve, reject) { return tslib_1.__awaiter(_this, void 0, void 0, function () {
return tslib_1.__generator(this, function (_a) {
try {
if (response.data && typeof response.data.getReader === "function") {
return [2 /*return*/, this.handleBrowserReadableStream(response.data, resolve, reject, response, streamConfig)];
}
if (typeof response.data === "string") {
return [2 /*return*/, this.handleBrowserStringResponse(response.data, resolve, response, streamConfig)];
}
if (typeof response.data === "object" && response.data !== null) {
this.logger.debug("No stream found, but data object is available");
this.invokeOnCompleteCallback(response.data, streamConfig);
return [2 /*return*/, resolve(response.data)];
}
this.logger.error("No usable data found in response");
return [2 /*return*/, reject(new Error("No usable data found in response"))];
}
catch (error) {
this.logger.error("Error processing streaming response: ".concat(error));
this.invokeOnErrorCallback(error, streamConfig);
reject(error);
}
return [2 /*return*/];
});
}); });
};
ApiCall.prototype.handleBrowserReadableStream = function (stream, resolve, reject, response, streamConfig) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var reader, allChunks, buffer, _a, done, value, lines_1, chunk, lines, error_2;
return tslib_1.__generator(this, function (_b) {
switch (_b.label) {
case 0:
this.logger.debug("Found ReadableStream in response.data");
reader = stream.getReader();
allChunks = [];
buffer = "";
_b.label = 1;
case 1:
_b.trys.push([1, 5, , 6]);
_b.label = 2;
case 2:
if (!true) return [3 /*break*/, 4];
return [4 /*yield*/, reader.read()];
case 3:
_a = _b.sent(), done = _a.done, value = _a.value;
if (done) {
this.logger.debug("Stream reading complete");
if (buffer.trim()) {
lines_1 = buffer.split("\n");
this.processStreamLines(lines_1, allChunks, streamConfig);
}
return [3 /*break*/, 4];
}
chunk = new TextDecoder().decode(value);
this.logger.debug("Received chunk: ".concat(chunk.length, " bytes"));
buffer += chunk;
lines = buffer.split("\n");
buffer = lines.pop() || "";
this.processStreamLines(lines, allChunks, streamConfig);
return [3 /*break*/, 2];
case 4:
this.finalizeStreamResult(allChunks, resolve, response, streamConfig);
return [3 /*break*/, 6];
case 5:
error_2 = _b.sent();
this.logger.error("Stream error: ".concat(error_2));
this.invokeOnErrorCallback(error_2, streamConfig);
reject(error_2);
return [3 /*break*/, 6];
case 6: return [2 /*return*/];
}
});
});
};
ApiCall.prototype.handleBrowserStringResponse = function (data, resolve, response, streamConfig) {
this.logger.debug("Processing text response as stream data");
var allChunks = [];
var lines = data.split("\n");
this.processStreamLines(lines, allChunks, streamConfig);
if (allChunks.length > 0) {
var finalResult = this.combineStreamingChunks(allChunks);
this.invokeOnCompleteCallback(finalResult, streamConfig);
resolve(finalResult);
}
else {
// If no chunks were processed, use the original response
this.logger.debug("No chunks processed, returning original API response");
this.invokeOnCompleteCallback(response.data, streamConfig);
resolve(response.data);
}
};
ApiCall.prototype.processStreamLines = function (lines, allChunks, streamConfig) {
for (var _i = 0, lines_2 = lines; _i < lines_2.length; _i++) {
var line = lines_2[_i];
if (line.trim() && line !== "data: [DONE]") {
var processed = this.processStreamingLine(line);
if (processed !== null) {
this.invokeOnChunkCallback(processed, streamConfig);
allChunks.push(processed);
}
}
}
};
ApiCall.prototype.finalizeStreamResult = function (allChunks, resolve, response, streamConfig) {
if (allChunks.length > 0) {
var finalResult = this.combineStreamingChunks(allChunks);
this.logger.debug("Stream processing complete");
this.invokeOnCompleteCallback(finalResult, streamConfig);
resolve(finalResult);
}
else {
this.logger.debug("No chunks processed, returning original API response");
this.invokeOnCompleteCallback(response.data, streamConfig);
resolve(response.data);
}
};
/**
* Combines multiple streaming chunks into a single coherent result
* This is critical for ensuring we return the complete data rather than just the last chunk
*/
ApiCall.prototype.combineStreamingChunks = function (chunks) {
if (chunks.length === 0)
return {};
if (chunks.length === 1)
return chunks[0];
// For conversation streams with message chunks
var messagesChunks = this.getMessageChunks(chunks);
if (messagesChunks.length > 0) {
return this.combineMessageChunks(chunks, messagesChunks);
}
// For regular search responses
var lastChunk = chunks[chunks.length - 1];
if (!this.isCompleteSearchResponse(lastChunk)) {
throw new Error("Last chunk is not a complete search response");
}
return lastChunk;
};
ApiCall.prototype.getMessageChunks = function (chunks) {
return chunks.filter(this.isChunkMessage);
};
ApiCall.prototype.isChunkMessage = function (chunk) {
return (typeof chunk === "object" &&
chunk !== null &&
"message" in chunk &&
"conversation_id" in chunk);
};
ApiCall.prototype.combineMessageChunks = function (chunks, messagesChunks) {
this.logger.debug("Found ".concat(messagesChunks.length, " message chunks to combine"));
var lastChunk = chunks[chunks.length - 1];
if (this.isCompleteSearchResponse(lastChunk)) {
return lastChunk;
}
var metadataChunk = chunks.find(this.isCompleteSearchResponse);
if (!metadataChunk) {
throw new Error("No metadata chunk found");
}
return metadataChunk;
};
ApiCall.prototype.isCompleteSearchResponse = function (chunk) {
if (typeof chunk === "object" &&
chunk !== null &&
Object.keys(chunk).length > 0) {
return ("results" in chunk ||
"found" in chunk ||
"hits" in chunk ||
"page" in chunk ||
"search_time_ms" in chunk);
}
return false;
};
// Attempts to find the next healthy node, looping through the list of nodes once.
// But if no healthy nodes are found, it will just return the next node, even if it's unhealthy
// so we can try the request for good measure, in case that node has become healthy since
ApiCall.prototype.getNextNode = function (requestNumber) {
if (requestNumber === void 0) { requestNumber = 0; }
// Check if nearestNode is set and is healthy, if so return it
if (this.nearestNode != null) {
this.logger.debug("Request #".concat(requestNumber, ": Nodes Health: Node ").concat(this.nearestNode.index, " is ").concat(this.nearestNode.isHealthy === true ? "Healthy" : "Unhealthy"));
if (this.nearestNode.isHealthy === true ||
this.nodeDueForHealthcheck(this.nearestNode, requestNumber)) {
this.logger.debug("Request #".concat(requestNumber, ": Updated current node to Node ").concat(this.nearestNode.index));
return this.nearestNode;
}
this.logger.debug("Request #".concat(requestNumber, ": Falling back to individual nodes"));
}
// Fallback to nodes as usual
this.logger.debug("Request #".concat(requestNumber, ": Nodes Health: ").concat(this.nodes
.map(function (node) {
return "Node ".concat(node.index, " is ").concat(node.isHealthy === true ? "Healthy" : "Unhealthy");
})
.join(" || ")));
var candidateNode = this.nodes[0];
for (var i = 0; i <= this.nodes.length; i++) {
this.currentNodeIndex = (this.currentNodeIndex + 1) % this.nodes.length;
candidateNode = this.nodes[this.currentNodeIndex];
if (candidateNode.isHealthy === true ||
this.nodeDueForHealthcheck(candidateNode, requestNumber)) {
this.logger.debug("Request #".concat(requestNumber, ": Updated current node to Node ").concat(candidateNode.index));
return candidateNode;
}
}
// None of the nodes are marked healthy, but some of them could have become healthy since last health check.
// So we will just return the next node.
this.logger.debug("Request #".concat(requestNumber, ": No healthy nodes were found. Returning the next node, Node ").concat(candidateNode.index));
return candidateNode;
};
ApiCall.prototype.nodeDueForHealthcheck = function (node, requestNumber) {
if (requestNumber === void 0) { requestNumber = 0; }
var isDueForHealthcheck = Date.now() - node.lastAccessTimestamp >
this.healthcheckIntervalSeconds * 1000;
if (isDueForHealthcheck) {
this.logger.debug("Request #".concat(requestNumber, ": Node ").concat(node.index, " has exceeded healtcheckIntervalSeconds of ").concat(this.healthcheckIntervalSeconds, ". Adding it back into rotation."));
}
return isDueForHealthcheck;
};
ApiCall.prototype.initializeMetadataForNodes = function () {
var _this = this;
if (this.nearestNode != null) {
this.nearestNode.index = "nearestNode";
this.setNodeHealthcheck(this.nearestNode, HEALTHY);
}
this.nodes.forEach(function (node, i) {
node.index = i;
_this.setNodeHealthcheck(node, HEALTHY);
});
};
ApiCall.prototype.setNodeHealthcheck = function (node, isHealthy) {
node.isHealthy = isHealthy;
node.lastAccessTimestamp = Date.now();
};
ApiCall.prototype.uriFor = function (endpoint, node) {
if (node.url != null) {
return "".concat(node.url).concat(endpoint);
}
return "".concat(node.protocol, "://").concat(node.host, ":").concat(node.port).concat(node.path).concat(endpoint);
};
ApiCall.prototype.defaultHeaders = function () {
var defaultHeaders = {};
if (!this.sendApiKeyAsQueryParam) {
defaultHeaders[APIKEYHEADERNAME] = this.apiKey;
}
defaultHeaders["Content-Type"] = "application/json";
return defaultHeaders;
};
ApiCall.prototype.timer = function (seconds) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
return tslib_1.__generator(this, function (_a) {
return [2 /*return*/, new Promise(function (resolve) { return setTimeout(resolve, seconds * 1000); })];
});
});
};
ApiCall.prototype.customErrorForResponse = function (response, messageFromServer, httpBody) {
var errorMessage = "Request failed with HTTP code ".concat(response.status);
if (typeof messageFromServer === "string" &&
messageFromServer.trim() !== "") {
errorMessage += " | Server said: ".concat(messageFromServer);
}
var error = new TypesenseError_1.default(errorMessage, httpBody, response.status);
if (response.status === 400) {
error = new Errors_1.RequestMalformed(errorMessage, httpBody, response.status);
}
else if (response.status === 401) {
error = new Errors_1.RequestUnauthorized(errorMessage, httpBody, response.status);
}
else if (response.status === 404) {
error = new Errors_1.ObjectNotFound(errorMessage, httpBody, response.status);
}
else if (response.status === 409) {
error = new Errors_1.ObjectAlreadyExists(errorMessage, httpBody, response.status);
}
else if (response.status === 422) {
error = new Errors_1.ObjectUnprocessable(errorMessage, httpBody, response.status);
}
else if (response.status >= 500 && response.status <= 599) {
error = new Errors_1.ServerError(errorMessage, httpBody, response.status);
}
else {
error = new Errors_1.HTTPError(errorMessage, httpBody, response.status);
}
return error;
};
ApiCall.prototype.invokeOnChunkCallback = function (data, streamConfig) {
if (streamConfig === null || streamConfig === void 0 ? void 0 : streamConfig.onChunk) {
try {
streamConfig.onChunk(data);
}
catch (error) {
this.logger.warn("Error in onChunk callback: ".concat(error));
}
}
};
ApiCall.prototype.invokeOnCompleteCallback = function (data, streamConfig) {
if (streamConfig === null || streamConfig === void 0 ? void 0 : streamConfig.onComplete) {
try {
streamConfig.onComplete(data);
}
catch (error) {
this.logger.warn("Error in onComplete callback: ".concat(error));
}
}
};
ApiCall.prototype.invokeOnErrorCallback = function (error, streamConfig) {
if (streamConfig === null || streamConfig === void 0 ? void 0 : streamConfig.onError) {
var errorObj = (0, Utils_1.toErrorWithMessage)(error);
try {
streamConfig.onError(errorObj);
}
catch (callbackError) {
this.logger.warn("Error in onError callback: ".concat(callbackError));
}
}
};
return ApiCall;
}());
exports.default = ApiCall;
//# sourceMappingURL=ApiCall.js.map