typesense
Version:
Javascript Library for Typesense
329 lines (294 loc) • 9.84 kB
text/typescript
import * as logger from "loglevel";
import { Logger, LogLevelDesc } from "loglevel";
import { MissingConfigurationError } from "./Errors";
import type { Agent as HTTPAgent } from "http";
import type { Agent as HTTPSAgent } from "https";
import type { AxiosRequestConfig } from "axios";
import { DocumentSchema, SearchResponse } from "./Documents";
export interface NodeConfiguration {
host: string;
port: number;
protocol: string;
path?: string;
url?: string;
}
export interface NodeConfigurationWithHostname {
host: string;
port: number;
protocol: string;
path?: string;
}
export interface NodeConfigurationWithUrl {
url: string;
}
export interface ConfigurationOptions {
apiKey: string;
nodes:
| NodeConfiguration[]
| NodeConfigurationWithHostname[]
| NodeConfigurationWithUrl[];
randomizeNodes?: boolean;
/**
* @deprecated
* masterNode is now consolidated to nodes, starting with Typesense Server v0.12'
*/
masterNode?:
| NodeConfiguration
| NodeConfigurationWithHostname
| NodeConfigurationWithUrl;
/**
* @deprecated
* readReplicaNodes is now consolidated to nodes, starting with Typesense Server v0.12'
*/
readReplicaNodes?:
| NodeConfiguration[]
| NodeConfigurationWithHostname[]
| NodeConfigurationWithUrl[];
nearestNode?:
| NodeConfiguration
| NodeConfigurationWithHostname
| NodeConfigurationWithUrl;
connectionTimeoutSeconds?: number;
timeoutSeconds?: number;
healthcheckIntervalSeconds?: number;
numRetries?: number;
retryIntervalSeconds?: number;
sendApiKeyAsQueryParam?: boolean | undefined;
useServerSideSearchCache?: boolean;
cacheSearchResultsForSeconds?: number;
additionalHeaders?: Record<string, string>;
logLevel?: LogLevelDesc;
logger?: Logger;
/**
* Set a custom HTTP Agent
*
* This is helpful for eg, to enable keepAlive which helps prevents ECONNRESET socket hang up errors
* Usage:
* const { Agent: HTTPAgent } = require("http");
* ...
* httpAgent: new HTTPAgent({ keepAlive: true }),
* @type {HTTPAgent}
*/
httpAgent?: HTTPAgent;
/**
* Set a custom HTTPS Agent
*
* This is helpful for eg, to enable keepAlive which helps prevents ECONNRESET socket hang up errors
* Usage:
* const { Agent: HTTPSAgent } = require("https");
* ...
* httpsAgent: new HTTPSAgent({ keepAlive: true }),
* @type {HTTPSAgent}
*/
httpsAgent?: HTTPSAgent;
/**
* Set a custom paramsSerializer
*
* See axios documentation for more information on how to use this parameter: https://axios-http.com/docs/req_config
* This is helpful for handling React Native issues like this: https://github.com/axios/axios/issues/6102#issuecomment-2085301397
* @type {any}
*/
paramsSerializer?: any;
/**
* Set a custom axios adapter
*
* Useful for customizing the underlying HTTP client library used by Typesense.
*
* For example, you can use this to use a custom HTTP client library like `fetch`, in order for the library to work on the edge.
* Related GiHub issue: https://github.com/typesense/typesense-js/issues/161
*
* See axios documentation for more information on how to use this parameter: https://axios-http.com/docs/req_config
*/
axiosAdapter?: AxiosRequestConfig["adapter"];
}
/**
* Configuration options for streaming responses
*/
export interface BaseStreamConfig {
/**
* Callback function that will be called for each chunk of data received
* during streaming
*/
onChunk?: (data: { conversation_id: string; message: string }) => void;
/**
* Callback function that will be called if there is an error during streaming
*/
onError?: (error: Error) => void;
}
/**
* Stream configuration for standard search responses
* For specialized responses like MultiSearch, extend BaseStreamConfig with the appropriate onComplete signature
*/
export interface StreamConfig<T extends DocumentSchema>
extends BaseStreamConfig {
/**
* Callback function that will be called when the streaming is complete
*/
onComplete?: (data: SearchResponse<T>) => void;
}
export default class Configuration {
readonly nodes:
| NodeConfiguration[]
| NodeConfigurationWithHostname[]
| NodeConfigurationWithUrl[];
readonly nearestNode?:
| NodeConfiguration
| NodeConfigurationWithHostname
| NodeConfigurationWithUrl;
readonly connectionTimeoutSeconds: number;
readonly healthcheckIntervalSeconds: number;
readonly numRetries: number;
readonly retryIntervalSeconds: number;
readonly apiKey: string;
readonly sendApiKeyAsQueryParam?: boolean;
readonly cacheSearchResultsForSeconds: number;
readonly useServerSideSearchCache: boolean;
readonly logger: Logger;
readonly logLevel: LogLevelDesc;
readonly additionalHeaders?: Record<string, string>;
readonly httpAgent?: HTTPAgent;
readonly httpsAgent?: HTTPSAgent;
readonly paramsSerializer?: any;
readonly axiosAdapter?: AxiosRequestConfig["adapter"];
constructor(options: ConfigurationOptions) {
this.nodes = options.nodes || [];
this.nodes = this.nodes
.map((node) => this.setDefaultPathInNode(node))
.map((node) => this.setDefaultPortInNode(node))
.map((node) => ({ ...node })) as NodeConfiguration[]; // Make a deep copy
if (options.randomizeNodes == null) {
options.randomizeNodes = true;
}
if (options.randomizeNodes === true) {
this.shuffleArray(this.nodes);
}
this.nearestNode = options.nearestNode;
this.nearestNode = this.setDefaultPathInNode(this.nearestNode);
this.nearestNode = this.setDefaultPortInNode(this.nearestNode);
this.connectionTimeoutSeconds =
options.connectionTimeoutSeconds || options.timeoutSeconds || 5;
this.healthcheckIntervalSeconds = options.healthcheckIntervalSeconds || 60;
this.numRetries =
(options.numRetries !== undefined && options.numRetries >= 0
? options.numRetries
: this.nodes.length + (this.nearestNode == null ? 0 : 1)) || 3;
this.retryIntervalSeconds = options.retryIntervalSeconds || 0.1;
this.apiKey = options.apiKey;
this.sendApiKeyAsQueryParam = options.sendApiKeyAsQueryParam; // We will set a default for this in Client and SearchClient
this.cacheSearchResultsForSeconds =
options.cacheSearchResultsForSeconds || 0; // Disable client-side cache by default
this.useServerSideSearchCache = options.useServerSideSearchCache || false;
this.axiosAdapter = options.axiosAdapter;
this.logger = options.logger || logger;
this.logLevel = options.logLevel || "warn";
this.logger.setLevel(this.logLevel);
this.additionalHeaders = options.additionalHeaders;
this.httpAgent = options.httpAgent;
this.httpsAgent = options.httpsAgent;
this.paramsSerializer = options.paramsSerializer;
this.showDeprecationWarnings(options);
this.validate();
}
validate(): boolean {
if (this.nodes == null || this.nodes.length === 0 || this.validateNodes()) {
throw new MissingConfigurationError(
"Ensure that nodes[].protocol, nodes[].host and nodes[].port are set",
);
}
if (
this.nearestNode != null &&
this.isNodeMissingAnyParameters(this.nearestNode)
) {
throw new MissingConfigurationError(
"Ensure that nearestNodes.protocol, nearestNodes.host and nearestNodes.port are set",
);
}
if (this.apiKey == null) {
throw new MissingConfigurationError("Ensure that apiKey is set");
}
return true;
}
private validateNodes(): boolean {
return this.nodes.some((node) => {
return this.isNodeMissingAnyParameters(node);
});
}
private isNodeMissingAnyParameters(
node:
| NodeConfiguration
| NodeConfigurationWithHostname
| NodeConfigurationWithUrl,
): boolean {
return (
!["protocol", "host", "port", "path"].every((key) => {
return node.hasOwnProperty(key);
}) && node["url"] == null
);
}
private setDefaultPathInNode(
node:
| NodeConfiguration
| NodeConfigurationWithHostname
| NodeConfigurationWithUrl
| undefined,
):
| NodeConfiguration
| NodeConfigurationWithHostname
| NodeConfigurationWithUrl
| undefined {
if (node != null && !node.hasOwnProperty("path")) {
node["path"] = "";
}
return node;
}
private setDefaultPortInNode(
node:
| NodeConfiguration
| NodeConfigurationWithHostname
| NodeConfigurationWithUrl
| undefined,
):
| NodeConfiguration
| NodeConfigurationWithHostname
| NodeConfigurationWithUrl
| undefined {
if (
node != null &&
!node.hasOwnProperty("port") &&
node.hasOwnProperty("protocol")
) {
switch (node["protocol"]) {
case "https":
node["port"] = 443;
break;
case "http":
node["port"] = 80;
break;
}
}
return node;
}
private showDeprecationWarnings(options: ConfigurationOptions): void {
if (options.timeoutSeconds) {
this.logger.warn(
"Deprecation warning: timeoutSeconds is now renamed to connectionTimeoutSeconds",
);
}
if (options.masterNode) {
this.logger.warn(
"Deprecation warning: masterNode is now consolidated to nodes, starting with Typesense Server v0.12",
);
}
if (options.readReplicaNodes) {
this.logger.warn(
"Deprecation warning: readReplicaNodes is now consolidated to nodes, starting with Typesense Server v0.12",
);
}
}
private shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
}