From d773af2a09af6774130ff16f86ef5de632461dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Vannicatte?= Date: Thu, 20 Jan 2022 13:34:10 +0100 Subject: [PATCH] feat(js): client common --- .github/actions/cache/action.yml | 6 + clients/README.md | 4 +- .../client-abtesting/package.json | 6 +- .../client-abtesting/src/abtestingApi.ts | 12 +- .../client-abtesting/src/apis.ts | 4 +- .../client-analytics/package.json | 6 +- .../client-analytics/src/analyticsApi.ts | 12 +- .../client-analytics/src/apis.ts | 4 +- .../client-analytics/utils/Response.ts | 23 -- .../client-analytics/utils/StatefulHost.ts | 34 --- .../client-analytics/utils/Transporter.ts | 243 ------------------ .../client-analytics/utils/cache/Cache.ts | 27 -- .../utils/cache/MemoryCache.ts | 39 --- .../client-analytics/utils/errors.ts | 38 --- .../client-analytics/utils/helpers.ts | 117 --------- .../utils/requester/EchoRequester.ts | 49 ---- .../utils/requester/HttpRequester.ts | 94 ------- .../utils/requester/Requester.ts | 8 - .../client-analytics/utils/stackTrace.ts | 30 --- .../client-analytics/utils/types.ts | 73 ------ .../client-common/index.ts | 9 + .../client-common/package.json | 23 ++ .../utils => client-common/src}/Response.ts | 0 .../src}/StatefulHost.ts | 0 .../src}/Transporter.ts | 0 .../src}/cache/Cache.ts | 0 .../src}/cache/MemoryCache.ts | 0 .../client-common/src/cache/index.ts | 2 + .../utils => client-common/src}/errors.ts | 0 .../utils => client-common/src}/helpers.ts | 0 .../src}/requester/EchoRequester.ts | 0 .../src}/requester/HttpRequester.ts | 0 .../src}/requester/Requester.ts | 0 .../client-common/src/requester/index.ts | 3 + .../utils => client-common/src}/stackTrace.ts | 0 .../utils => client-common/src}/types.ts | 0 .../client-common/tsconfig.json | 22 ++ .../client-insights/package.json | 6 +- .../client-insights/src/apis.ts | 4 +- .../client-insights/src/insightsApi.ts | 12 +- .../client-insights/utils/Response.ts | 23 -- .../client-insights/utils/StatefulHost.ts | 34 --- .../client-insights/utils/Transporter.ts | 243 ------------------ .../client-insights/utils/cache/Cache.ts | 27 -- .../utils/cache/MemoryCache.ts | 39 --- .../client-insights/utils/errors.ts | 38 --- .../client-insights/utils/helpers.ts | 117 --------- .../utils/requester/EchoRequester.ts | 49 ---- .../utils/requester/HttpRequester.ts | 94 ------- .../utils/requester/Requester.ts | 8 - .../client-insights/utils/stackTrace.ts | 30 --- .../client-insights/utils/types.ts | 73 ------ .../client-personalization/package.json | 6 +- .../client-personalization/src/apis.ts | 4 +- .../src/personalizationApi.ts | 12 +- .../client-personalization/utils/Response.ts | 23 -- .../utils/StatefulHost.ts | 34 --- .../utils/Transporter.ts | 243 ------------------ .../utils/cache/Cache.ts | 27 -- .../utils/cache/MemoryCache.ts | 39 --- .../client-personalization/utils/errors.ts | 38 --- .../client-personalization/utils/helpers.ts | 117 --------- .../utils/requester/EchoRequester.ts | 49 ---- .../utils/requester/HttpRequester.ts | 94 ------- .../utils/requester/Requester.ts | 8 - .../utils/stackTrace.ts | 30 --- .../client-personalization/utils/types.ts | 73 ------ .../client-query-suggestions/package.json | 6 +- .../client-query-suggestions/src/apis.ts | 4 +- .../src/querySuggestionsApi.ts | 12 +- .../utils/Response.ts | 23 -- .../utils/StatefulHost.ts | 34 --- .../utils/Transporter.ts | 243 ------------------ .../utils/cache/Cache.ts | 27 -- .../utils/cache/MemoryCache.ts | 39 --- .../client-query-suggestions/utils/errors.ts | 38 --- .../client-query-suggestions/utils/helpers.ts | 117 --------- .../utils/requester/EchoRequester.ts | 49 ---- .../utils/requester/HttpRequester.ts | 94 ------- .../utils/requester/Requester.ts | 8 - .../utils/stackTrace.ts | 30 --- .../client-query-suggestions/utils/types.ts | 73 ------ .../client-search/package.json | 6 +- .../client-search/src/apis.ts | 4 +- .../client-search/src/searchApi.ts | 13 +- .../client-search/utils/Response.ts | 23 -- .../client-search/utils/StatefulHost.ts | 34 --- .../client-search/utils/Transporter.ts | 243 ------------------ .../client-search/utils/cache/Cache.ts | 27 -- .../client-search/utils/cache/MemoryCache.ts | 39 --- .../client-search/utils/errors.ts | 38 --- .../client-search/utils/helpers.ts | 117 --------- .../utils/requester/EchoRequester.ts | 49 ---- .../utils/requester/HttpRequester.ts | 94 ------- .../utils/requester/Requester.ts | 8 - .../client-search/utils/stackTrace.ts | 30 --- .../client-search/utils/types.ts | 73 ------ .../recommend/package.json | 6 +- .../recommend/src/apis.ts | 4 +- .../recommend/src/recommendApi.ts | 13 +- .../recommend/utils/Response.ts | 23 -- .../recommend/utils/StatefulHost.ts | 34 --- .../recommend/utils/Transporter.ts | 243 ------------------ .../recommend/utils/cache/Cache.ts | 27 -- .../recommend/utils/cache/MemoryCache.ts | 39 --- .../recommend/utils/errors.ts | 38 --- .../recommend/utils/helpers.ts | 117 --------- .../utils/requester/EchoRequester.ts | 49 ---- .../utils/requester/HttpRequester.ts | 94 ------- .../recommend/utils/requester/Requester.ts | 8 - .../recommend/utils/stackTrace.ts | 30 --- .../recommend/utils/types.ts | 73 ------ .../utils/Response.ts | 23 -- .../utils/StatefulHost.ts | 34 --- .../utils/Transporter.ts | 243 ------------------ .../utils/cache/Cache.ts | 27 -- .../utils/cache/MemoryCache.ts | 39 --- .../utils/errors.ts | 38 --- .../utils/helpers.ts | 117 --------- .../utils/requester/EchoRequester.ts | 49 ---- .../utils/requester/HttpRequester.ts | 94 ------- .../utils/requester/Requester.ts | 8 - .../utils/stackTrace.ts | 30 --- .../utils/types.ts | 73 ------ doc/contribution_addNewClient.md | 2 +- package.json | 2 +- scripts/post-gen/global.sh | 17 ++ scripts/post-gen/javascript.sh | 5 - templates/javascript/api-all.mustache | 4 +- templates/javascript/api-single.mustache | 6 +- templates/javascript/package.mustache | 6 +- tests/package.json | 5 +- tests/src/methods/requests/cts.ts | 3 +- tests/src/methods/requests/generate.ts | 7 +- tests/src/methods/requests/main.ts | 3 +- yarn.lock | 16 ++ 136 files changed, 218 insertions(+), 5508 deletions(-) delete mode 100644 clients/algoliasearch-client-javascript/client-analytics/utils/Response.ts delete mode 100644 clients/algoliasearch-client-javascript/client-analytics/utils/StatefulHost.ts delete mode 100644 clients/algoliasearch-client-javascript/client-analytics/utils/Transporter.ts delete mode 100644 clients/algoliasearch-client-javascript/client-analytics/utils/cache/Cache.ts delete mode 100644 clients/algoliasearch-client-javascript/client-analytics/utils/cache/MemoryCache.ts delete mode 100644 clients/algoliasearch-client-javascript/client-analytics/utils/errors.ts delete mode 100644 clients/algoliasearch-client-javascript/client-analytics/utils/helpers.ts delete mode 100644 clients/algoliasearch-client-javascript/client-analytics/utils/requester/EchoRequester.ts delete mode 100644 clients/algoliasearch-client-javascript/client-analytics/utils/requester/HttpRequester.ts delete mode 100644 clients/algoliasearch-client-javascript/client-analytics/utils/requester/Requester.ts delete mode 100644 clients/algoliasearch-client-javascript/client-analytics/utils/stackTrace.ts delete mode 100644 clients/algoliasearch-client-javascript/client-analytics/utils/types.ts create mode 100644 clients/algoliasearch-client-javascript/client-common/index.ts create mode 100644 clients/algoliasearch-client-javascript/client-common/package.json rename clients/algoliasearch-client-javascript/{client-abtesting/utils => client-common/src}/Response.ts (100%) rename clients/algoliasearch-client-javascript/{client-abtesting/utils => client-common/src}/StatefulHost.ts (100%) rename clients/algoliasearch-client-javascript/{client-abtesting/utils => client-common/src}/Transporter.ts (100%) rename clients/algoliasearch-client-javascript/{client-abtesting/utils => client-common/src}/cache/Cache.ts (100%) rename clients/algoliasearch-client-javascript/{client-abtesting/utils => client-common/src}/cache/MemoryCache.ts (100%) create mode 100644 clients/algoliasearch-client-javascript/client-common/src/cache/index.ts rename clients/algoliasearch-client-javascript/{client-abtesting/utils => client-common/src}/errors.ts (100%) rename clients/algoliasearch-client-javascript/{client-abtesting/utils => client-common/src}/helpers.ts (100%) rename clients/algoliasearch-client-javascript/{client-abtesting/utils => client-common/src}/requester/EchoRequester.ts (100%) rename clients/algoliasearch-client-javascript/{client-abtesting/utils => client-common/src}/requester/HttpRequester.ts (100%) rename clients/algoliasearch-client-javascript/{client-abtesting/utils => client-common/src}/requester/Requester.ts (100%) create mode 100644 clients/algoliasearch-client-javascript/client-common/src/requester/index.ts rename clients/algoliasearch-client-javascript/{client-abtesting/utils => client-common/src}/stackTrace.ts (100%) rename clients/algoliasearch-client-javascript/{client-abtesting/utils => client-common/src}/types.ts (100%) create mode 100644 clients/algoliasearch-client-javascript/client-common/tsconfig.json delete mode 100644 clients/algoliasearch-client-javascript/client-insights/utils/Response.ts delete mode 100644 clients/algoliasearch-client-javascript/client-insights/utils/StatefulHost.ts delete mode 100644 clients/algoliasearch-client-javascript/client-insights/utils/Transporter.ts delete mode 100644 clients/algoliasearch-client-javascript/client-insights/utils/cache/Cache.ts delete mode 100644 clients/algoliasearch-client-javascript/client-insights/utils/cache/MemoryCache.ts delete mode 100644 clients/algoliasearch-client-javascript/client-insights/utils/errors.ts delete mode 100644 clients/algoliasearch-client-javascript/client-insights/utils/helpers.ts delete mode 100644 clients/algoliasearch-client-javascript/client-insights/utils/requester/EchoRequester.ts delete mode 100644 clients/algoliasearch-client-javascript/client-insights/utils/requester/HttpRequester.ts delete mode 100644 clients/algoliasearch-client-javascript/client-insights/utils/requester/Requester.ts delete mode 100644 clients/algoliasearch-client-javascript/client-insights/utils/stackTrace.ts delete mode 100644 clients/algoliasearch-client-javascript/client-insights/utils/types.ts delete mode 100644 clients/algoliasearch-client-javascript/client-personalization/utils/Response.ts delete mode 100644 clients/algoliasearch-client-javascript/client-personalization/utils/StatefulHost.ts delete mode 100644 clients/algoliasearch-client-javascript/client-personalization/utils/Transporter.ts delete mode 100644 clients/algoliasearch-client-javascript/client-personalization/utils/cache/Cache.ts delete mode 100644 clients/algoliasearch-client-javascript/client-personalization/utils/cache/MemoryCache.ts delete mode 100644 clients/algoliasearch-client-javascript/client-personalization/utils/errors.ts delete mode 100644 clients/algoliasearch-client-javascript/client-personalization/utils/helpers.ts delete mode 100644 clients/algoliasearch-client-javascript/client-personalization/utils/requester/EchoRequester.ts delete mode 100644 clients/algoliasearch-client-javascript/client-personalization/utils/requester/HttpRequester.ts delete mode 100644 clients/algoliasearch-client-javascript/client-personalization/utils/requester/Requester.ts delete mode 100644 clients/algoliasearch-client-javascript/client-personalization/utils/stackTrace.ts delete mode 100644 clients/algoliasearch-client-javascript/client-personalization/utils/types.ts delete mode 100644 clients/algoliasearch-client-javascript/client-query-suggestions/utils/Response.ts delete mode 100644 clients/algoliasearch-client-javascript/client-query-suggestions/utils/StatefulHost.ts delete mode 100644 clients/algoliasearch-client-javascript/client-query-suggestions/utils/Transporter.ts delete mode 100644 clients/algoliasearch-client-javascript/client-query-suggestions/utils/cache/Cache.ts delete mode 100644 clients/algoliasearch-client-javascript/client-query-suggestions/utils/cache/MemoryCache.ts delete mode 100644 clients/algoliasearch-client-javascript/client-query-suggestions/utils/errors.ts delete mode 100644 clients/algoliasearch-client-javascript/client-query-suggestions/utils/helpers.ts delete mode 100644 clients/algoliasearch-client-javascript/client-query-suggestions/utils/requester/EchoRequester.ts delete mode 100644 clients/algoliasearch-client-javascript/client-query-suggestions/utils/requester/HttpRequester.ts delete mode 100644 clients/algoliasearch-client-javascript/client-query-suggestions/utils/requester/Requester.ts delete mode 100644 clients/algoliasearch-client-javascript/client-query-suggestions/utils/stackTrace.ts delete mode 100644 clients/algoliasearch-client-javascript/client-query-suggestions/utils/types.ts delete mode 100644 clients/algoliasearch-client-javascript/client-search/utils/Response.ts delete mode 100644 clients/algoliasearch-client-javascript/client-search/utils/StatefulHost.ts delete mode 100644 clients/algoliasearch-client-javascript/client-search/utils/Transporter.ts delete mode 100644 clients/algoliasearch-client-javascript/client-search/utils/cache/Cache.ts delete mode 100644 clients/algoliasearch-client-javascript/client-search/utils/cache/MemoryCache.ts delete mode 100644 clients/algoliasearch-client-javascript/client-search/utils/errors.ts delete mode 100644 clients/algoliasearch-client-javascript/client-search/utils/helpers.ts delete mode 100644 clients/algoliasearch-client-javascript/client-search/utils/requester/EchoRequester.ts delete mode 100644 clients/algoliasearch-client-javascript/client-search/utils/requester/HttpRequester.ts delete mode 100644 clients/algoliasearch-client-javascript/client-search/utils/requester/Requester.ts delete mode 100644 clients/algoliasearch-client-javascript/client-search/utils/stackTrace.ts delete mode 100644 clients/algoliasearch-client-javascript/client-search/utils/types.ts delete mode 100644 clients/algoliasearch-client-javascript/recommend/utils/Response.ts delete mode 100644 clients/algoliasearch-client-javascript/recommend/utils/StatefulHost.ts delete mode 100644 clients/algoliasearch-client-javascript/recommend/utils/Transporter.ts delete mode 100644 clients/algoliasearch-client-javascript/recommend/utils/cache/Cache.ts delete mode 100644 clients/algoliasearch-client-javascript/recommend/utils/cache/MemoryCache.ts delete mode 100644 clients/algoliasearch-client-javascript/recommend/utils/errors.ts delete mode 100644 clients/algoliasearch-client-javascript/recommend/utils/helpers.ts delete mode 100644 clients/algoliasearch-client-javascript/recommend/utils/requester/EchoRequester.ts delete mode 100644 clients/algoliasearch-client-javascript/recommend/utils/requester/HttpRequester.ts delete mode 100644 clients/algoliasearch-client-javascript/recommend/utils/requester/Requester.ts delete mode 100644 clients/algoliasearch-client-javascript/recommend/utils/stackTrace.ts delete mode 100644 clients/algoliasearch-client-javascript/recommend/utils/types.ts delete mode 100644 clients/algoliasearch-client-javascript/utils/Response.ts delete mode 100644 clients/algoliasearch-client-javascript/utils/StatefulHost.ts delete mode 100644 clients/algoliasearch-client-javascript/utils/Transporter.ts delete mode 100644 clients/algoliasearch-client-javascript/utils/cache/Cache.ts delete mode 100644 clients/algoliasearch-client-javascript/utils/cache/MemoryCache.ts delete mode 100644 clients/algoliasearch-client-javascript/utils/errors.ts delete mode 100644 clients/algoliasearch-client-javascript/utils/helpers.ts delete mode 100644 clients/algoliasearch-client-javascript/utils/requester/EchoRequester.ts delete mode 100644 clients/algoliasearch-client-javascript/utils/requester/HttpRequester.ts delete mode 100644 clients/algoliasearch-client-javascript/utils/requester/Requester.ts delete mode 100644 clients/algoliasearch-client-javascript/utils/stackTrace.ts delete mode 100644 clients/algoliasearch-client-javascript/utils/types.ts diff --git a/.github/actions/cache/action.yml b/.github/actions/cache/action.yml index c9351159be7..8703f4849f3 100644 --- a/.github/actions/cache/action.yml +++ b/.github/actions/cache/action.yml @@ -26,6 +26,12 @@ runs: key: ${{ runner.os }}-${{ inputs.spec }}-specs-${{ hashFiles(format('specs/{0}/**', inputs.spec)) }} # restore clients + - name: Restore built JavaScript common client + uses: actions/cache@v2 + with: + path: /home/runner/work/api-clients-automation/api-clients-automation/clients/algoliasearch-client-javascript/client-common/dist + key: ${{ runner.os }}-1-js-client-common-${{ hashFiles('clients/common-client-javascript/client-common/**') }} + - name: Restore built JavaScript search client if: ${{ inputs.job == 'cts' }} uses: actions/cache@v2 diff --git a/clients/README.md b/clients/README.md index 6165e303b96..7f0bf4c72fa 100644 --- a/clients/README.md +++ b/clients/README.md @@ -1,6 +1,6 @@ # Clients -This folder hosts the generated clients and their utils. +This folder hosts the generated clients. ## Generated clients @@ -20,4 +20,4 @@ This folder hosts the generated clients and their utils. #### Utils -- [JavaScript](./algoliasearch-client-javascript/utils/): The JavaScript clients utils. +- [JavaScript](./algoliasearch-client-javascript/client-common/): The JavaScript clients common files. diff --git a/clients/algoliasearch-client-javascript/client-abtesting/package.json b/clients/algoliasearch-client-javascript/client-abtesting/package.json index 9d46b116962..16529da4c35 100644 --- a/clients/algoliasearch-client-javascript/client-abtesting/package.json +++ b/clients/algoliasearch-client-javascript/client-abtesting/package.json @@ -9,14 +9,16 @@ "main": "dist/api.js", "types": "dist/api.d.ts", "scripts": { - "clean": "rm -Rf node_modules/ *.js", "build": "tsc", - "test": "yarn build && node dist/client.js" + "clean": "rm -rf dist/" }, "engines": { "node": "^16.0.0", "yarn": "^3.0.0" }, + "dependencies": { + "@algolia/client-common": "5.0.0" + }, "devDependencies": { "@types/node": "16.11.11", "typescript": "4.5.4" diff --git a/clients/algoliasearch-client-javascript/client-abtesting/src/abtestingApi.ts b/clients/algoliasearch-client-javascript/client-abtesting/src/abtestingApi.ts index fbbdec0e207..c694b0eb9be 100644 --- a/clients/algoliasearch-client-javascript/client-abtesting/src/abtestingApi.ts +++ b/clients/algoliasearch-client-javascript/client-abtesting/src/abtestingApi.ts @@ -1,10 +1,16 @@ +import { Transporter } from '@algolia/client-common'; +import type { + Headers, + Requester, + Host, + Request, + RequestOptions, +} from '@algolia/client-common'; + import type { ABTest } from '../model/aBTest'; import type { ABTestResponse } from '../model/aBTestResponse'; import type { AddABTestsRequest } from '../model/addABTestsRequest'; import type { ListABTestsResponse } from '../model/listABTestsResponse'; -import { Transporter } from '../utils/Transporter'; -import type { Requester } from '../utils/requester/Requester'; -import type { Headers, Host, Request, RequestOptions } from '../utils/types'; export class AbtestingApi { protected authentications = { diff --git a/clients/algoliasearch-client-javascript/client-abtesting/src/apis.ts b/clients/algoliasearch-client-javascript/client-abtesting/src/apis.ts index 8709c6154e4..f61ac9a0137 100644 --- a/clients/algoliasearch-client-javascript/client-abtesting/src/apis.ts +++ b/clients/algoliasearch-client-javascript/client-abtesting/src/apis.ts @@ -1,8 +1,6 @@ import { AbtestingApi } from './abtestingApi'; export * from './abtestingApi'; -export * from '../utils/errors'; -export { EchoRequester } from '../utils/requester/EchoRequester'; -export { EchoResponse } from '../utils/types'; +export * from '@algolia/client-common'; export const APIS = [AbtestingApi]; diff --git a/clients/algoliasearch-client-javascript/client-analytics/package.json b/clients/algoliasearch-client-javascript/client-analytics/package.json index 6c9729f7658..8152c8d9f87 100644 --- a/clients/algoliasearch-client-javascript/client-analytics/package.json +++ b/clients/algoliasearch-client-javascript/client-analytics/package.json @@ -9,14 +9,16 @@ "main": "dist/api.js", "types": "dist/api.d.ts", "scripts": { - "clean": "rm -Rf node_modules/ *.js", "build": "tsc", - "test": "yarn build && node dist/client.js" + "clean": "rm -rf dist/" }, "engines": { "node": "^16.0.0", "yarn": "^3.0.0" }, + "dependencies": { + "@algolia/client-common": "5.0.0" + }, "devDependencies": { "@types/node": "16.11.11", "typescript": "4.5.4" diff --git a/clients/algoliasearch-client-javascript/client-analytics/src/analyticsApi.ts b/clients/algoliasearch-client-javascript/client-analytics/src/analyticsApi.ts index b813696d305..4d09971ddb3 100644 --- a/clients/algoliasearch-client-javascript/client-analytics/src/analyticsApi.ts +++ b/clients/algoliasearch-client-javascript/client-analytics/src/analyticsApi.ts @@ -1,3 +1,12 @@ +import { Transporter } from '@algolia/client-common'; +import type { + Headers, + Requester, + Host, + Request, + RequestOptions, +} from '@algolia/client-common'; + import type { GetAverageClickPositionResponse } from '../model/getAverageClickPositionResponse'; import type { GetClickPositionsResponse } from '../model/getClickPositionsResponse'; import type { GetClickThroughRateResponse } from '../model/getClickThroughRateResponse'; @@ -17,9 +26,6 @@ import type { GetTopHitsResponseWithAnalytics } from '../model/getTopHitsRespons import type { GetTopSearchesResponse } from '../model/getTopSearchesResponse'; import type { GetTopSearchesResponseWithAnalytics } from '../model/getTopSearchesResponseWithAnalytics'; import type { GetUsersCountResponse } from '../model/getUsersCountResponse'; -import { Transporter } from '../utils/Transporter'; -import type { Requester } from '../utils/requester/Requester'; -import type { Headers, Host, Request, RequestOptions } from '../utils/types'; export class AnalyticsApi { protected authentications = { diff --git a/clients/algoliasearch-client-javascript/client-analytics/src/apis.ts b/clients/algoliasearch-client-javascript/client-analytics/src/apis.ts index 9f62c9c5153..ddc6ef0ece4 100644 --- a/clients/algoliasearch-client-javascript/client-analytics/src/apis.ts +++ b/clients/algoliasearch-client-javascript/client-analytics/src/apis.ts @@ -1,8 +1,6 @@ import { AnalyticsApi } from './analyticsApi'; export * from './analyticsApi'; -export * from '../utils/errors'; -export { EchoRequester } from '../utils/requester/EchoRequester'; -export { EchoResponse } from '../utils/types'; +export * from '@algolia/client-common'; export const APIS = [AnalyticsApi]; diff --git a/clients/algoliasearch-client-javascript/client-analytics/utils/Response.ts b/clients/algoliasearch-client-javascript/client-analytics/utils/Response.ts deleted file mode 100644 index bd22de7df9e..00000000000 --- a/clients/algoliasearch-client-javascript/client-analytics/utils/Response.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Response } from './types'; - -export function isNetworkError({ - isTimedOut, - status, -}: Omit): boolean { - return !isTimedOut && ~~status === 0; -} - -export function isRetryable({ - isTimedOut, - status, -}: Omit): boolean { - return ( - isTimedOut || - isNetworkError({ isTimedOut, status }) || - (~~(status / 100) !== 2 && ~~(status / 100) !== 4) - ); -} - -export function isSuccess({ status }: Pick): boolean { - return ~~(status / 100) === 2; -} diff --git a/clients/algoliasearch-client-javascript/client-analytics/utils/StatefulHost.ts b/clients/algoliasearch-client-javascript/client-analytics/utils/StatefulHost.ts deleted file mode 100644 index 162c4ed1c66..00000000000 --- a/clients/algoliasearch-client-javascript/client-analytics/utils/StatefulHost.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Host } from './types'; - -const EXPIRATION_DELAY = 2 * 60 * 1000; - -export class StatefulHost implements Host { - url: string; - accept: 'read' | 'readWrite' | 'write'; - protocol: 'http' | 'https'; - - private lastUpdate: number; - private status: 'down' | 'timedout' | 'up'; - - constructor(host: Host, status: StatefulHost['status'] = 'up') { - this.url = host.url; - this.accept = host.accept; - this.protocol = host.protocol; - - this.status = status; - this.lastUpdate = Date.now(); - } - - isUp(): boolean { - return ( - this.status === 'up' || Date.now() - this.lastUpdate > EXPIRATION_DELAY - ); - } - - isTimedout(): boolean { - return ( - this.status === 'timedout' && - Date.now() - this.lastUpdate <= EXPIRATION_DELAY - ); - } -} diff --git a/clients/algoliasearch-client-javascript/client-analytics/utils/Transporter.ts b/clients/algoliasearch-client-javascript/client-analytics/utils/Transporter.ts deleted file mode 100644 index 48b4edebfdb..00000000000 --- a/clients/algoliasearch-client-javascript/client-analytics/utils/Transporter.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { isRetryable, isSuccess } from './Response'; -import { StatefulHost } from './StatefulHost'; -import type { Cache } from './cache/Cache'; -import { MemoryCache } from './cache/MemoryCache'; -import { RetryError } from './errors'; -import { - deserializeFailure, - deserializeSuccess, - serializeData, - serializeHeaders, - serializeUrl, -} from './helpers'; -import { HttpRequester } from './requester/HttpRequester'; -import type { Requester } from './requester/Requester'; -import { - stackTraceWithoutCredentials, - stackFrameWithoutCredentials, -} from './stackTrace'; -import type { - Headers, - Host, - Request, - RequestOptions, - StackFrame, - Timeouts, - Response, - EndRequest, -} from './types'; - -export class Transporter { - private hosts: Host[]; - private baseHeaders: Headers; - private hostsCache: Cache; - private userAgent: string; - private timeouts: Timeouts; - private requester: Requester; - - constructor({ - hosts, - baseHeaders, - userAgent, - timeouts, - requester = new HttpRequester(), - }: { - hosts: Host[]; - baseHeaders: Headers; - userAgent: string; - timeouts: Timeouts; - requester?: Requester; - }) { - this.hosts = hosts; - this.hostsCache = new MemoryCache(); - this.baseHeaders = baseHeaders; - this.userAgent = userAgent; - this.timeouts = timeouts; - this.requester = requester; - } - - setHosts(hosts: Host[]): void { - this.hosts = hosts; - this.hostsCache.clear(); - } - - setRequester(requester: Requester): void { - this.requester = requester; - } - - async createRetryableOptions(compatibleHosts: Host[]): Promise<{ - hosts: Host[]; - getTimeout: (retryCount: number, timeout: number) => number; - }> { - const statefulHosts = await Promise.all( - compatibleHosts.map((statelessHost) => { - return this.hostsCache.get(statelessHost, () => { - return Promise.resolve(new StatefulHost(statelessHost)); - }); - }) - ); - const hostsUp = statefulHosts.filter((host) => host.isUp()); - const hostsTimeouted = statefulHosts.filter((host) => host.isTimedout()); - - /** - * Note, we put the hosts that previously timeouted on the end of the list. - */ - const hostsAvailable = [...hostsUp, ...hostsTimeouted]; - - const hosts = hostsAvailable.length > 0 ? hostsAvailable : compatibleHosts; - - return { - hosts, - getTimeout(timeoutsCount: number, baseTimeout: number): number { - /** - * Imagine that you have 4 hosts, if timeouts will increase - * on the following way: 1 (timeouted) > 4 (timeouted) > 5 (200). - * - * Note that, the very next request, we start from the previous timeout. - * - * 5 (timeouted) > 6 (timeouted) > 7 ... - * - * This strategy may need to be reviewed, but is the strategy on the our - * current v3 version. - */ - const timeoutMultiplier = - hostsTimeouted.length === 0 && timeoutsCount === 0 - ? 1 - : hostsTimeouted.length + 3 + timeoutsCount; - - return timeoutMultiplier * baseTimeout; - }, - }; - } - - async request( - request: Request, - requestOptions: RequestOptions - ): Promise { - const stackTrace: StackFrame[] = []; - - const isRead = request.method === 'GET'; - - /** - * First we prepare the payload that do not depend from hosts. - */ - const data = serializeData(request, requestOptions); - const headers = serializeHeaders(this.baseHeaders, requestOptions); - const method = request.method; - - // On `GET`, the data is proxied to query parameters. - const dataQueryParameters: Record = isRead - ? { - ...request.data, - ...requestOptions.data, - } - : {}; - - const queryParameters = { - 'x-algolia-agent': this.userAgent, - ...dataQueryParameters, - ...requestOptions.queryParameters, - }; - - let timeoutsCount = 0; - - const retry = async ( - hosts: Host[], - getTimeout: (timeoutsCount: number, timeout: number) => number - ): Promise => { - /** - * We iterate on each host, until there is no host left. - */ - const host = hosts.pop(); - if (host === undefined) { - throw new RetryError(stackTraceWithoutCredentials(stackTrace)); - } - - let responseTimeout = requestOptions.timeout; - if (responseTimeout === undefined) { - responseTimeout = isRead ? this.timeouts.read : this.timeouts.write; - } - - const payload: EndRequest = { - data, - headers, - method, - url: serializeUrl(host, request.path, queryParameters), - connectTimeout: getTimeout(timeoutsCount, this.timeouts.connect), - responseTimeout: getTimeout(timeoutsCount, responseTimeout), - }; - - /** - * The stackFrame is pushed to the stackTrace so we - * can have information about onRetry and onFailure - * decisions. - */ - const pushToStackTrace = (response: Response): StackFrame => { - const stackFrame: StackFrame = { - request: payload, - response, - host, - triesLeft: hosts.length, - }; - - stackTrace.push(stackFrame); - - return stackFrame; - }; - - const response = await this.requester.send(payload, request); - - if (isRetryable(response)) { - const stackFrame = pushToStackTrace(response); - - // If response is a timeout, we increase the number of timeouts so we can increase the timeout later. - if (response.isTimedOut) { - timeoutsCount++; - } - /** - * Failures are individually sent to the logger, allowing - * the end user to debug / store stack frames even - * when a retry error does not happen. - */ - // eslint-disable-next-line no-console -- this will be fixed with the new `Logger` - console.log( - 'Retryable failure', - stackFrameWithoutCredentials(stackFrame) - ); - - /** - * We also store the state of the host in failure cases. If the host, is - * down it will remain down for the next 2 minutes. In a timeout situation, - * this host will be added end of the list of hosts on the next request. - */ - await this.hostsCache.set( - host, - new StatefulHost(host, response.isTimedOut ? 'timedout' : 'down') - ); - return retry(hosts, getTimeout); - } - if (isSuccess(response)) { - return deserializeSuccess(response); - } - - pushToStackTrace(response); - throw deserializeFailure(response, stackTrace); - }; - - /** - * Finally, for each retryable host perform request until we got a non - * retryable response. Some notes here: - * - * 1. The reverse here is applied so we can apply a `pop` later on => more performant. - * 2. We also get from the retryable options a timeout multiplier that is tailored - * for the current context. - */ - const compatibleHosts = this.hosts.filter( - (host) => - host.accept === 'readWrite' || - (isRead ? host.accept === 'read' : host.accept === 'write') - ); - const options = await this.createRetryableOptions(compatibleHosts); - return retry([...options.hosts].reverse(), options.getTimeout); - } -} diff --git a/clients/algoliasearch-client-javascript/client-analytics/utils/cache/Cache.ts b/clients/algoliasearch-client-javascript/client-analytics/utils/cache/Cache.ts deleted file mode 100644 index 625862660c6..00000000000 --- a/clients/algoliasearch-client-javascript/client-analytics/utils/cache/Cache.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface Cache { - /** - * Gets the value of the given `key`. - */ - get: ( - key: Record | string, - defaultValue: () => Promise - ) => Promise; - - /** - * Sets the given value with the given `key`. - */ - set: ( - key: Record | string, - value: TValue - ) => Promise; - - /** - * Deletes the given `key`. - */ - delete: (key: Record | string) => Promise; - - /** - * Clears the cache. - */ - clear: () => Promise; -} diff --git a/clients/algoliasearch-client-javascript/client-analytics/utils/cache/MemoryCache.ts b/clients/algoliasearch-client-javascript/client-analytics/utils/cache/MemoryCache.ts deleted file mode 100644 index f7853f39bc4..00000000000 --- a/clients/algoliasearch-client-javascript/client-analytics/utils/cache/MemoryCache.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Cache } from './Cache'; - -export class MemoryCache implements Cache { - private cache: Record = {}; - - async get( - key: Record | string, - defaultValue: () => Promise - ): Promise { - const keyAsString = JSON.stringify(key); - - if (keyAsString in this.cache) { - return Promise.resolve(this.cache[keyAsString]); - } - - return await defaultValue(); - } - - set( - key: Record | string, - value: TValue - ): Promise { - this.cache[JSON.stringify(key)] = value; - - return Promise.resolve(value); - } - - delete(key: Record | string): Promise { - delete this.cache[JSON.stringify(key)]; - - return Promise.resolve(); - } - - clear(): Promise { - this.cache = {}; - - return Promise.resolve(); - } -} diff --git a/clients/algoliasearch-client-javascript/client-analytics/utils/errors.ts b/clients/algoliasearch-client-javascript/client-analytics/utils/errors.ts deleted file mode 100644 index a02f3004ad6..00000000000 --- a/clients/algoliasearch-client-javascript/client-analytics/utils/errors.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Response, StackFrame } from './types'; - -class ErrorWithStackTrace extends Error { - stackTrace: StackFrame[]; - - constructor(message: string, stackTrace: StackFrame[]) { - super(message); - // the array and object should be frozen to reflect the stackTrace at the time of the error - this.stackTrace = stackTrace; - } -} - -export class RetryError extends ErrorWithStackTrace { - constructor(stackTrace: StackFrame[]) { - super( - 'Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.', - stackTrace - ); - } -} - -export class ApiError extends ErrorWithStackTrace { - status: number; - - constructor(message: string, status: number, stackTrace: StackFrame[]) { - super(message, stackTrace); - this.status = status; - } -} - -export class DeserializationError extends Error { - response: Response; - - constructor(message: string, response: Response) { - super(message); - this.response = response; - } -} diff --git a/clients/algoliasearch-client-javascript/client-analytics/utils/helpers.ts b/clients/algoliasearch-client-javascript/client-analytics/utils/helpers.ts deleted file mode 100644 index 5d64ac88688..00000000000 --- a/clients/algoliasearch-client-javascript/client-analytics/utils/helpers.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { ApiError, DeserializationError } from './errors'; -import type { - Headers, - Host, - Request, - RequestOptions, - Response, - StackFrame, -} from './types'; - -export function shuffle(array: TData[]): TData[] { - const shuffledArray = array; - - for (let c = array.length - 1; c > 0; c--) { - const b = Math.floor(Math.random() * (c + 1)); - const a = array[c]; - - shuffledArray[c] = array[b]; - shuffledArray[b] = a; - } - - return shuffledArray; -} - -export function serializeUrl( - host: Host, - path: string, - queryParameters: Readonly> -): string { - const queryParametersAsString = serializeQueryParameters(queryParameters); - let url = `${host.protocol}://${host.url}/${ - path.charAt(0) === '/' ? path.substr(1) : path - }`; - - if (queryParametersAsString.length) { - url += `?${queryParametersAsString}`; - } - - return url; -} - -export function serializeQueryParameters( - parameters: Readonly> -): string { - const isObjectOrArray = (value: any): boolean => - Object.prototype.toString.call(value) === '[object Object]' || - Object.prototype.toString.call(value) === '[object Array]'; - - return Object.keys(parameters) - .map( - (key) => - `${key}=${ - isObjectOrArray(parameters[key]) - ? JSON.stringify(parameters[key]) - : parameters[key] - }` - ) - .join('&'); -} - -export function serializeData( - request: Request, - requestOptions: RequestOptions -): string | undefined { - if ( - request.method === 'GET' || - (request.data === undefined && requestOptions.data === undefined) - ) { - return undefined; - } - - const data = Array.isArray(request.data) - ? request.data - : { ...request.data, ...requestOptions.data }; - - return JSON.stringify(data); -} - -export function serializeHeaders( - baseHeaders: Headers, - requestOptions: RequestOptions -): Headers { - const headers: Headers = { - ...baseHeaders, - ...requestOptions.headers, - }; - const serializedHeaders: Headers = {}; - - Object.keys(headers).forEach((header) => { - const value = headers[header]; - serializedHeaders[header.toLowerCase()] = value; - }); - - return serializedHeaders; -} - -export function deserializeSuccess(response: Response): TObject { - try { - return JSON.parse(response.content); - } catch (e) { - throw new DeserializationError((e as Error).message, response); - } -} - -export function deserializeFailure( - { content, status }: Response, - stackFrame: StackFrame[] -): Error { - let message = content; - try { - message = JSON.parse(content).message; - } catch (e) { - // .. - } - - return new ApiError(message, status, stackFrame); -} diff --git a/clients/algoliasearch-client-javascript/client-analytics/utils/requester/EchoRequester.ts b/clients/algoliasearch-client-javascript/client-analytics/utils/requester/EchoRequester.ts deleted file mode 100644 index f773d0f6379..00000000000 --- a/clients/algoliasearch-client-javascript/client-analytics/utils/requester/EchoRequester.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { EndRequest, Request, Response, EchoResponse } from '../types'; - -import { Requester } from './Requester'; - -function searchParamsWithoutUA( - params: URLSearchParams -): EchoResponse['searchParams'] { - const searchParams = {}; - - for (const [k, v] of params) { - if (k === 'x-algolia-agent') { - continue; - } - - searchParams[k] = v; - } - - return Object.entries(searchParams).length === 0 ? undefined : searchParams; -} - -export class EchoRequester extends Requester { - constructor(private status = 200) { - super(); - } - - send( - { headers, url, connectTimeout, responseTimeout }: EndRequest, - { data, ...originalRequest }: Request - ): Promise { - const urlSearchParams = new URL(url).searchParams; - const userAgent = urlSearchParams.get('x-algolia-agent') || undefined; - const originalData = - data && Object.entries(data).length > 0 ? data : undefined; - - return Promise.resolve({ - content: JSON.stringify({ - ...originalRequest, - headers, - connectTimeout, - responseTimeout, - userAgent: userAgent ? encodeURI(userAgent) : undefined, - searchParams: searchParamsWithoutUA(urlSearchParams), - data: originalData, - }), - isTimedOut: false, - status: this.status, - }); - } -} diff --git a/clients/algoliasearch-client-javascript/client-analytics/utils/requester/HttpRequester.ts b/clients/algoliasearch-client-javascript/client-analytics/utils/requester/HttpRequester.ts deleted file mode 100644 index 3697d290fb7..00000000000 --- a/clients/algoliasearch-client-javascript/client-analytics/utils/requester/HttpRequester.ts +++ /dev/null @@ -1,94 +0,0 @@ -import http from 'http'; -import https from 'https'; - -import type { EndRequest, Response } from '../types'; - -import { Requester } from './Requester'; - -// Global agents allow us to reuse the TCP protocol with multiple clients -const agentOptions = { keepAlive: true }; -const httpAgent = new http.Agent(agentOptions); -const httpsAgent = new https.Agent(agentOptions); - -export class HttpRequester extends Requester { - send(request: EndRequest): Promise { - return new Promise((resolve) => { - let responseTimeout: NodeJS.Timeout | undefined; - // eslint-disable-next-line prefer-const -- linter thinks this is not reassigned - let connectTimeout: NodeJS.Timeout | undefined; - const url = new URL(request.url); - const path = - url.search === null ? url.pathname : `${url.pathname}${url.search}`; - const options: https.RequestOptions = { - agent: url.protocol === 'https:' ? httpsAgent : httpAgent, - hostname: url.hostname, - path, - method: request.method, - headers: request.headers, - ...(url.port !== undefined ? { port: url.port || '' } : {}), - }; - - const req = (url.protocol === 'https:' ? https : http).request( - options, - (response) => { - let contentBuffers: Buffer[] = []; - - response.on('data', (chunk) => { - contentBuffers = contentBuffers.concat(chunk); - }); - - response.on('end', () => { - clearTimeout(connectTimeout as NodeJS.Timeout); - clearTimeout(responseTimeout as NodeJS.Timeout); - - resolve({ - status: response.statusCode || 0, - content: Buffer.concat(contentBuffers).toString(), - isTimedOut: false, - }); - }); - } - ); - - const createTimeout = ( - timeout: number, - content: string - ): NodeJS.Timeout => { - return setTimeout(() => { - req.destroy(); - - resolve({ - status: 0, - content, - isTimedOut: true, - }); - }, timeout * 1000); - }; - - connectTimeout = createTimeout( - request.connectTimeout, - 'Connection timeout' - ); - - req.on('error', (error) => { - clearTimeout(connectTimeout as NodeJS.Timeout); - clearTimeout(responseTimeout!); - resolve({ status: 0, content: error.message, isTimedOut: false }); - }); - - req.once('response', () => { - clearTimeout(connectTimeout as NodeJS.Timeout); - responseTimeout = createTimeout( - request.responseTimeout, - 'Socket timeout' - ); - }); - - if (request.data !== undefined) { - req.write(request.data); - } - - req.end(); - }); - } -} diff --git a/clients/algoliasearch-client-javascript/client-analytics/utils/requester/Requester.ts b/clients/algoliasearch-client-javascript/client-analytics/utils/requester/Requester.ts deleted file mode 100644 index 41c06065753..00000000000 --- a/clients/algoliasearch-client-javascript/client-analytics/utils/requester/Requester.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { EndRequest, Request, Response } from '../types'; - -export abstract class Requester { - abstract send( - request: EndRequest, - originalRequest: Request - ): Promise; -} diff --git a/clients/algoliasearch-client-javascript/client-analytics/utils/stackTrace.ts b/clients/algoliasearch-client-javascript/client-analytics/utils/stackTrace.ts deleted file mode 100644 index 14750a54f22..00000000000 --- a/clients/algoliasearch-client-javascript/client-analytics/utils/stackTrace.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { StackFrame } from './types'; - -export function stackTraceWithoutCredentials( - stackTrace: StackFrame[] -): StackFrame[] { - return stackTrace.map((stackFrame) => - stackFrameWithoutCredentials(stackFrame) - ); -} - -export function stackFrameWithoutCredentials( - stackFrame: StackFrame -): StackFrame { - const modifiedHeaders: Record = stackFrame.request.headers[ - 'x-algolia-api-key' - ] - ? { 'x-algolia-api-key': '*****' } - : {}; - - return { - ...stackFrame, - request: { - ...stackFrame.request, - headers: { - ...stackFrame.request.headers, - ...modifiedHeaders, - }, - }, - }; -} diff --git a/clients/algoliasearch-client-javascript/client-analytics/utils/types.ts b/clients/algoliasearch-client-javascript/client-analytics/utils/types.ts deleted file mode 100644 index 20136fe367b..00000000000 --- a/clients/algoliasearch-client-javascript/client-analytics/utils/types.ts +++ /dev/null @@ -1,73 +0,0 @@ -export type Method = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT'; - -export type Request = { - method: Method; - path: string; - data?: Record; -}; - -export type RequestOptions = { - /** - * Custom timeout for the request. Note that, in normal situacions - * the given timeout will be applied. But the transporter layer may - * increase this timeout if there is need for it. - */ - timeout?: number; - - /** - * Custom headers for the request. This headers are - * going to be merged the transporter headers. - */ - headers?: Record; - - /** - * Custom query parameters for the request. This query parameters are - * going to be merged the transporter query parameters. - */ - queryParameters: Record; - data?: Record; -}; - -export type EndRequest = { - method: Method; - url: string; - connectTimeout: number; - responseTimeout: number; - headers: Headers; - data?: string; -}; - -export type Response = { - content: string; - isTimedOut: boolean; - status: number; -}; - -export type EchoResponse = Request & { - connectTimeout: number; - headers: Record; - responseTimeout: number; - searchParams?: Record; - userAgent?: string; -}; - -export type Headers = Record; - -export type Host = { - url: string; - accept: 'read' | 'readWrite' | 'write'; - protocol: 'http' | 'https'; -}; - -export type StackFrame = { - request: EndRequest; - response: Response; - host: Host; - triesLeft: number; -}; - -export type Timeouts = { - readonly connect: number; - readonly read: number; - readonly write: number; -}; diff --git a/clients/algoliasearch-client-javascript/client-common/index.ts b/clients/algoliasearch-client-javascript/client-common/index.ts new file mode 100644 index 00000000000..74854d51141 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-common/index.ts @@ -0,0 +1,9 @@ +export * from './src/cache'; +export * from './src/errors'; +export * from './src/helpers'; +export * from './src/requester'; +export * from './src/Response'; +export * from './src/stackTrace'; +export * from './src/StatefulHost'; +export * from './src/Transporter'; +export * from './src/types'; diff --git a/clients/algoliasearch-client-javascript/client-common/package.json b/clients/algoliasearch-client-javascript/client-common/package.json new file mode 100644 index 00000000000..ef64f26e23f --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-common/package.json @@ -0,0 +1,23 @@ +{ + "name": "@algolia/client-common", + "version": "5.0.0", + "description": "Common package for the Algolia JavaScript API client.", + "repository": "algolia/algoliasearch-client-javascript", + "author": "Algolia", + "private": true, + "license": "MIT", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "clean": "rm -rf dist/" + }, + "engines": { + "node": "^16.0.0", + "yarn": "^3.0.0" + }, + "devDependencies": { + "@types/node": "16.11.11", + "typescript": "4.5.4" + } +} diff --git a/clients/algoliasearch-client-javascript/client-abtesting/utils/Response.ts b/clients/algoliasearch-client-javascript/client-common/src/Response.ts similarity index 100% rename from clients/algoliasearch-client-javascript/client-abtesting/utils/Response.ts rename to clients/algoliasearch-client-javascript/client-common/src/Response.ts diff --git a/clients/algoliasearch-client-javascript/client-abtesting/utils/StatefulHost.ts b/clients/algoliasearch-client-javascript/client-common/src/StatefulHost.ts similarity index 100% rename from clients/algoliasearch-client-javascript/client-abtesting/utils/StatefulHost.ts rename to clients/algoliasearch-client-javascript/client-common/src/StatefulHost.ts diff --git a/clients/algoliasearch-client-javascript/client-abtesting/utils/Transporter.ts b/clients/algoliasearch-client-javascript/client-common/src/Transporter.ts similarity index 100% rename from clients/algoliasearch-client-javascript/client-abtesting/utils/Transporter.ts rename to clients/algoliasearch-client-javascript/client-common/src/Transporter.ts diff --git a/clients/algoliasearch-client-javascript/client-abtesting/utils/cache/Cache.ts b/clients/algoliasearch-client-javascript/client-common/src/cache/Cache.ts similarity index 100% rename from clients/algoliasearch-client-javascript/client-abtesting/utils/cache/Cache.ts rename to clients/algoliasearch-client-javascript/client-common/src/cache/Cache.ts diff --git a/clients/algoliasearch-client-javascript/client-abtesting/utils/cache/MemoryCache.ts b/clients/algoliasearch-client-javascript/client-common/src/cache/MemoryCache.ts similarity index 100% rename from clients/algoliasearch-client-javascript/client-abtesting/utils/cache/MemoryCache.ts rename to clients/algoliasearch-client-javascript/client-common/src/cache/MemoryCache.ts diff --git a/clients/algoliasearch-client-javascript/client-common/src/cache/index.ts b/clients/algoliasearch-client-javascript/client-common/src/cache/index.ts new file mode 100644 index 00000000000..a0d120f82b8 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-common/src/cache/index.ts @@ -0,0 +1,2 @@ +export * from './Cache'; +export * from './MemoryCache'; diff --git a/clients/algoliasearch-client-javascript/client-abtesting/utils/errors.ts b/clients/algoliasearch-client-javascript/client-common/src/errors.ts similarity index 100% rename from clients/algoliasearch-client-javascript/client-abtesting/utils/errors.ts rename to clients/algoliasearch-client-javascript/client-common/src/errors.ts diff --git a/clients/algoliasearch-client-javascript/client-abtesting/utils/helpers.ts b/clients/algoliasearch-client-javascript/client-common/src/helpers.ts similarity index 100% rename from clients/algoliasearch-client-javascript/client-abtesting/utils/helpers.ts rename to clients/algoliasearch-client-javascript/client-common/src/helpers.ts diff --git a/clients/algoliasearch-client-javascript/client-abtesting/utils/requester/EchoRequester.ts b/clients/algoliasearch-client-javascript/client-common/src/requester/EchoRequester.ts similarity index 100% rename from clients/algoliasearch-client-javascript/client-abtesting/utils/requester/EchoRequester.ts rename to clients/algoliasearch-client-javascript/client-common/src/requester/EchoRequester.ts diff --git a/clients/algoliasearch-client-javascript/client-abtesting/utils/requester/HttpRequester.ts b/clients/algoliasearch-client-javascript/client-common/src/requester/HttpRequester.ts similarity index 100% rename from clients/algoliasearch-client-javascript/client-abtesting/utils/requester/HttpRequester.ts rename to clients/algoliasearch-client-javascript/client-common/src/requester/HttpRequester.ts diff --git a/clients/algoliasearch-client-javascript/client-abtesting/utils/requester/Requester.ts b/clients/algoliasearch-client-javascript/client-common/src/requester/Requester.ts similarity index 100% rename from clients/algoliasearch-client-javascript/client-abtesting/utils/requester/Requester.ts rename to clients/algoliasearch-client-javascript/client-common/src/requester/Requester.ts diff --git a/clients/algoliasearch-client-javascript/client-common/src/requester/index.ts b/clients/algoliasearch-client-javascript/client-common/src/requester/index.ts new file mode 100644 index 00000000000..c50d4513ebd --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-common/src/requester/index.ts @@ -0,0 +1,3 @@ +export * from './EchoRequester'; +export * from './HttpRequester'; +export * from './Requester'; diff --git a/clients/algoliasearch-client-javascript/client-abtesting/utils/stackTrace.ts b/clients/algoliasearch-client-javascript/client-common/src/stackTrace.ts similarity index 100% rename from clients/algoliasearch-client-javascript/client-abtesting/utils/stackTrace.ts rename to clients/algoliasearch-client-javascript/client-common/src/stackTrace.ts diff --git a/clients/algoliasearch-client-javascript/client-abtesting/utils/types.ts b/clients/algoliasearch-client-javascript/client-common/src/types.ts similarity index 100% rename from clients/algoliasearch-client-javascript/client-abtesting/utils/types.ts rename to clients/algoliasearch-client-javascript/client-common/src/types.ts diff --git a/clients/algoliasearch-client-javascript/client-common/tsconfig.json b/clients/algoliasearch-client-javascript/client-common/tsconfig.json new file mode 100644 index 00000000000..b32907408a2 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-common/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "noImplicitAny": false, + "suppressImplicitAnyIndexErrors": true, + "target": "ES6", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "strict": true, + "moduleResolution": "node", + "removeComments": true, + "sourceMap": true, + "noLib": false, + "declaration": true, + "lib": ["dom", "es6", "es5", "dom.iterable", "scripthost"], + "outDir": "dist", + "typeRoots": ["node_modules/@types"], + "types": ["node"] + }, + "include": ["src", "index.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/clients/algoliasearch-client-javascript/client-insights/package.json b/clients/algoliasearch-client-javascript/client-insights/package.json index 987293db90b..bd7e8c31669 100644 --- a/clients/algoliasearch-client-javascript/client-insights/package.json +++ b/clients/algoliasearch-client-javascript/client-insights/package.json @@ -9,14 +9,16 @@ "main": "dist/api.js", "types": "dist/api.d.ts", "scripts": { - "clean": "rm -Rf node_modules/ *.js", "build": "tsc", - "test": "yarn build && node dist/client.js" + "clean": "rm -rf dist/" }, "engines": { "node": "^16.0.0", "yarn": "^3.0.0" }, + "dependencies": { + "@algolia/client-common": "5.0.0" + }, "devDependencies": { "@types/node": "16.11.11", "typescript": "4.5.4" diff --git a/clients/algoliasearch-client-javascript/client-insights/src/apis.ts b/clients/algoliasearch-client-javascript/client-insights/src/apis.ts index d0561908c01..c77ef8ef28a 100644 --- a/clients/algoliasearch-client-javascript/client-insights/src/apis.ts +++ b/clients/algoliasearch-client-javascript/client-insights/src/apis.ts @@ -1,8 +1,6 @@ import { InsightsApi } from './insightsApi'; export * from './insightsApi'; -export * from '../utils/errors'; -export { EchoRequester } from '../utils/requester/EchoRequester'; -export { EchoResponse } from '../utils/types'; +export * from '@algolia/client-common'; export const APIS = [InsightsApi]; diff --git a/clients/algoliasearch-client-javascript/client-insights/src/insightsApi.ts b/clients/algoliasearch-client-javascript/client-insights/src/insightsApi.ts index 7b80fc359c0..35cbcac7dab 100644 --- a/clients/algoliasearch-client-javascript/client-insights/src/insightsApi.ts +++ b/clients/algoliasearch-client-javascript/client-insights/src/insightsApi.ts @@ -1,8 +1,14 @@ +import { Transporter } from '@algolia/client-common'; +import type { + Headers, + Requester, + Host, + Request, + RequestOptions, +} from '@algolia/client-common'; + import type { InsightEvents } from '../model/insightEvents'; import type { PushEventsResponse } from '../model/pushEventsResponse'; -import { Transporter } from '../utils/Transporter'; -import type { Requester } from '../utils/requester/Requester'; -import type { Headers, Host, Request, RequestOptions } from '../utils/types'; export class InsightsApi { protected authentications = { diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/Response.ts b/clients/algoliasearch-client-javascript/client-insights/utils/Response.ts deleted file mode 100644 index bd22de7df9e..00000000000 --- a/clients/algoliasearch-client-javascript/client-insights/utils/Response.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Response } from './types'; - -export function isNetworkError({ - isTimedOut, - status, -}: Omit): boolean { - return !isTimedOut && ~~status === 0; -} - -export function isRetryable({ - isTimedOut, - status, -}: Omit): boolean { - return ( - isTimedOut || - isNetworkError({ isTimedOut, status }) || - (~~(status / 100) !== 2 && ~~(status / 100) !== 4) - ); -} - -export function isSuccess({ status }: Pick): boolean { - return ~~(status / 100) === 2; -} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/StatefulHost.ts b/clients/algoliasearch-client-javascript/client-insights/utils/StatefulHost.ts deleted file mode 100644 index 162c4ed1c66..00000000000 --- a/clients/algoliasearch-client-javascript/client-insights/utils/StatefulHost.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Host } from './types'; - -const EXPIRATION_DELAY = 2 * 60 * 1000; - -export class StatefulHost implements Host { - url: string; - accept: 'read' | 'readWrite' | 'write'; - protocol: 'http' | 'https'; - - private lastUpdate: number; - private status: 'down' | 'timedout' | 'up'; - - constructor(host: Host, status: StatefulHost['status'] = 'up') { - this.url = host.url; - this.accept = host.accept; - this.protocol = host.protocol; - - this.status = status; - this.lastUpdate = Date.now(); - } - - isUp(): boolean { - return ( - this.status === 'up' || Date.now() - this.lastUpdate > EXPIRATION_DELAY - ); - } - - isTimedout(): boolean { - return ( - this.status === 'timedout' && - Date.now() - this.lastUpdate <= EXPIRATION_DELAY - ); - } -} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/Transporter.ts b/clients/algoliasearch-client-javascript/client-insights/utils/Transporter.ts deleted file mode 100644 index 48b4edebfdb..00000000000 --- a/clients/algoliasearch-client-javascript/client-insights/utils/Transporter.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { isRetryable, isSuccess } from './Response'; -import { StatefulHost } from './StatefulHost'; -import type { Cache } from './cache/Cache'; -import { MemoryCache } from './cache/MemoryCache'; -import { RetryError } from './errors'; -import { - deserializeFailure, - deserializeSuccess, - serializeData, - serializeHeaders, - serializeUrl, -} from './helpers'; -import { HttpRequester } from './requester/HttpRequester'; -import type { Requester } from './requester/Requester'; -import { - stackTraceWithoutCredentials, - stackFrameWithoutCredentials, -} from './stackTrace'; -import type { - Headers, - Host, - Request, - RequestOptions, - StackFrame, - Timeouts, - Response, - EndRequest, -} from './types'; - -export class Transporter { - private hosts: Host[]; - private baseHeaders: Headers; - private hostsCache: Cache; - private userAgent: string; - private timeouts: Timeouts; - private requester: Requester; - - constructor({ - hosts, - baseHeaders, - userAgent, - timeouts, - requester = new HttpRequester(), - }: { - hosts: Host[]; - baseHeaders: Headers; - userAgent: string; - timeouts: Timeouts; - requester?: Requester; - }) { - this.hosts = hosts; - this.hostsCache = new MemoryCache(); - this.baseHeaders = baseHeaders; - this.userAgent = userAgent; - this.timeouts = timeouts; - this.requester = requester; - } - - setHosts(hosts: Host[]): void { - this.hosts = hosts; - this.hostsCache.clear(); - } - - setRequester(requester: Requester): void { - this.requester = requester; - } - - async createRetryableOptions(compatibleHosts: Host[]): Promise<{ - hosts: Host[]; - getTimeout: (retryCount: number, timeout: number) => number; - }> { - const statefulHosts = await Promise.all( - compatibleHosts.map((statelessHost) => { - return this.hostsCache.get(statelessHost, () => { - return Promise.resolve(new StatefulHost(statelessHost)); - }); - }) - ); - const hostsUp = statefulHosts.filter((host) => host.isUp()); - const hostsTimeouted = statefulHosts.filter((host) => host.isTimedout()); - - /** - * Note, we put the hosts that previously timeouted on the end of the list. - */ - const hostsAvailable = [...hostsUp, ...hostsTimeouted]; - - const hosts = hostsAvailable.length > 0 ? hostsAvailable : compatibleHosts; - - return { - hosts, - getTimeout(timeoutsCount: number, baseTimeout: number): number { - /** - * Imagine that you have 4 hosts, if timeouts will increase - * on the following way: 1 (timeouted) > 4 (timeouted) > 5 (200). - * - * Note that, the very next request, we start from the previous timeout. - * - * 5 (timeouted) > 6 (timeouted) > 7 ... - * - * This strategy may need to be reviewed, but is the strategy on the our - * current v3 version. - */ - const timeoutMultiplier = - hostsTimeouted.length === 0 && timeoutsCount === 0 - ? 1 - : hostsTimeouted.length + 3 + timeoutsCount; - - return timeoutMultiplier * baseTimeout; - }, - }; - } - - async request( - request: Request, - requestOptions: RequestOptions - ): Promise { - const stackTrace: StackFrame[] = []; - - const isRead = request.method === 'GET'; - - /** - * First we prepare the payload that do not depend from hosts. - */ - const data = serializeData(request, requestOptions); - const headers = serializeHeaders(this.baseHeaders, requestOptions); - const method = request.method; - - // On `GET`, the data is proxied to query parameters. - const dataQueryParameters: Record = isRead - ? { - ...request.data, - ...requestOptions.data, - } - : {}; - - const queryParameters = { - 'x-algolia-agent': this.userAgent, - ...dataQueryParameters, - ...requestOptions.queryParameters, - }; - - let timeoutsCount = 0; - - const retry = async ( - hosts: Host[], - getTimeout: (timeoutsCount: number, timeout: number) => number - ): Promise => { - /** - * We iterate on each host, until there is no host left. - */ - const host = hosts.pop(); - if (host === undefined) { - throw new RetryError(stackTraceWithoutCredentials(stackTrace)); - } - - let responseTimeout = requestOptions.timeout; - if (responseTimeout === undefined) { - responseTimeout = isRead ? this.timeouts.read : this.timeouts.write; - } - - const payload: EndRequest = { - data, - headers, - method, - url: serializeUrl(host, request.path, queryParameters), - connectTimeout: getTimeout(timeoutsCount, this.timeouts.connect), - responseTimeout: getTimeout(timeoutsCount, responseTimeout), - }; - - /** - * The stackFrame is pushed to the stackTrace so we - * can have information about onRetry and onFailure - * decisions. - */ - const pushToStackTrace = (response: Response): StackFrame => { - const stackFrame: StackFrame = { - request: payload, - response, - host, - triesLeft: hosts.length, - }; - - stackTrace.push(stackFrame); - - return stackFrame; - }; - - const response = await this.requester.send(payload, request); - - if (isRetryable(response)) { - const stackFrame = pushToStackTrace(response); - - // If response is a timeout, we increase the number of timeouts so we can increase the timeout later. - if (response.isTimedOut) { - timeoutsCount++; - } - /** - * Failures are individually sent to the logger, allowing - * the end user to debug / store stack frames even - * when a retry error does not happen. - */ - // eslint-disable-next-line no-console -- this will be fixed with the new `Logger` - console.log( - 'Retryable failure', - stackFrameWithoutCredentials(stackFrame) - ); - - /** - * We also store the state of the host in failure cases. If the host, is - * down it will remain down for the next 2 minutes. In a timeout situation, - * this host will be added end of the list of hosts on the next request. - */ - await this.hostsCache.set( - host, - new StatefulHost(host, response.isTimedOut ? 'timedout' : 'down') - ); - return retry(hosts, getTimeout); - } - if (isSuccess(response)) { - return deserializeSuccess(response); - } - - pushToStackTrace(response); - throw deserializeFailure(response, stackTrace); - }; - - /** - * Finally, for each retryable host perform request until we got a non - * retryable response. Some notes here: - * - * 1. The reverse here is applied so we can apply a `pop` later on => more performant. - * 2. We also get from the retryable options a timeout multiplier that is tailored - * for the current context. - */ - const compatibleHosts = this.hosts.filter( - (host) => - host.accept === 'readWrite' || - (isRead ? host.accept === 'read' : host.accept === 'write') - ); - const options = await this.createRetryableOptions(compatibleHosts); - return retry([...options.hosts].reverse(), options.getTimeout); - } -} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/cache/Cache.ts b/clients/algoliasearch-client-javascript/client-insights/utils/cache/Cache.ts deleted file mode 100644 index 625862660c6..00000000000 --- a/clients/algoliasearch-client-javascript/client-insights/utils/cache/Cache.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface Cache { - /** - * Gets the value of the given `key`. - */ - get: ( - key: Record | string, - defaultValue: () => Promise - ) => Promise; - - /** - * Sets the given value with the given `key`. - */ - set: ( - key: Record | string, - value: TValue - ) => Promise; - - /** - * Deletes the given `key`. - */ - delete: (key: Record | string) => Promise; - - /** - * Clears the cache. - */ - clear: () => Promise; -} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/cache/MemoryCache.ts b/clients/algoliasearch-client-javascript/client-insights/utils/cache/MemoryCache.ts deleted file mode 100644 index f7853f39bc4..00000000000 --- a/clients/algoliasearch-client-javascript/client-insights/utils/cache/MemoryCache.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Cache } from './Cache'; - -export class MemoryCache implements Cache { - private cache: Record = {}; - - async get( - key: Record | string, - defaultValue: () => Promise - ): Promise { - const keyAsString = JSON.stringify(key); - - if (keyAsString in this.cache) { - return Promise.resolve(this.cache[keyAsString]); - } - - return await defaultValue(); - } - - set( - key: Record | string, - value: TValue - ): Promise { - this.cache[JSON.stringify(key)] = value; - - return Promise.resolve(value); - } - - delete(key: Record | string): Promise { - delete this.cache[JSON.stringify(key)]; - - return Promise.resolve(); - } - - clear(): Promise { - this.cache = {}; - - return Promise.resolve(); - } -} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/errors.ts b/clients/algoliasearch-client-javascript/client-insights/utils/errors.ts deleted file mode 100644 index a02f3004ad6..00000000000 --- a/clients/algoliasearch-client-javascript/client-insights/utils/errors.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Response, StackFrame } from './types'; - -class ErrorWithStackTrace extends Error { - stackTrace: StackFrame[]; - - constructor(message: string, stackTrace: StackFrame[]) { - super(message); - // the array and object should be frozen to reflect the stackTrace at the time of the error - this.stackTrace = stackTrace; - } -} - -export class RetryError extends ErrorWithStackTrace { - constructor(stackTrace: StackFrame[]) { - super( - 'Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.', - stackTrace - ); - } -} - -export class ApiError extends ErrorWithStackTrace { - status: number; - - constructor(message: string, status: number, stackTrace: StackFrame[]) { - super(message, stackTrace); - this.status = status; - } -} - -export class DeserializationError extends Error { - response: Response; - - constructor(message: string, response: Response) { - super(message); - this.response = response; - } -} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/helpers.ts b/clients/algoliasearch-client-javascript/client-insights/utils/helpers.ts deleted file mode 100644 index 5d64ac88688..00000000000 --- a/clients/algoliasearch-client-javascript/client-insights/utils/helpers.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { ApiError, DeserializationError } from './errors'; -import type { - Headers, - Host, - Request, - RequestOptions, - Response, - StackFrame, -} from './types'; - -export function shuffle(array: TData[]): TData[] { - const shuffledArray = array; - - for (let c = array.length - 1; c > 0; c--) { - const b = Math.floor(Math.random() * (c + 1)); - const a = array[c]; - - shuffledArray[c] = array[b]; - shuffledArray[b] = a; - } - - return shuffledArray; -} - -export function serializeUrl( - host: Host, - path: string, - queryParameters: Readonly> -): string { - const queryParametersAsString = serializeQueryParameters(queryParameters); - let url = `${host.protocol}://${host.url}/${ - path.charAt(0) === '/' ? path.substr(1) : path - }`; - - if (queryParametersAsString.length) { - url += `?${queryParametersAsString}`; - } - - return url; -} - -export function serializeQueryParameters( - parameters: Readonly> -): string { - const isObjectOrArray = (value: any): boolean => - Object.prototype.toString.call(value) === '[object Object]' || - Object.prototype.toString.call(value) === '[object Array]'; - - return Object.keys(parameters) - .map( - (key) => - `${key}=${ - isObjectOrArray(parameters[key]) - ? JSON.stringify(parameters[key]) - : parameters[key] - }` - ) - .join('&'); -} - -export function serializeData( - request: Request, - requestOptions: RequestOptions -): string | undefined { - if ( - request.method === 'GET' || - (request.data === undefined && requestOptions.data === undefined) - ) { - return undefined; - } - - const data = Array.isArray(request.data) - ? request.data - : { ...request.data, ...requestOptions.data }; - - return JSON.stringify(data); -} - -export function serializeHeaders( - baseHeaders: Headers, - requestOptions: RequestOptions -): Headers { - const headers: Headers = { - ...baseHeaders, - ...requestOptions.headers, - }; - const serializedHeaders: Headers = {}; - - Object.keys(headers).forEach((header) => { - const value = headers[header]; - serializedHeaders[header.toLowerCase()] = value; - }); - - return serializedHeaders; -} - -export function deserializeSuccess(response: Response): TObject { - try { - return JSON.parse(response.content); - } catch (e) { - throw new DeserializationError((e as Error).message, response); - } -} - -export function deserializeFailure( - { content, status }: Response, - stackFrame: StackFrame[] -): Error { - let message = content; - try { - message = JSON.parse(content).message; - } catch (e) { - // .. - } - - return new ApiError(message, status, stackFrame); -} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/requester/EchoRequester.ts b/clients/algoliasearch-client-javascript/client-insights/utils/requester/EchoRequester.ts deleted file mode 100644 index f773d0f6379..00000000000 --- a/clients/algoliasearch-client-javascript/client-insights/utils/requester/EchoRequester.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { EndRequest, Request, Response, EchoResponse } from '../types'; - -import { Requester } from './Requester'; - -function searchParamsWithoutUA( - params: URLSearchParams -): EchoResponse['searchParams'] { - const searchParams = {}; - - for (const [k, v] of params) { - if (k === 'x-algolia-agent') { - continue; - } - - searchParams[k] = v; - } - - return Object.entries(searchParams).length === 0 ? undefined : searchParams; -} - -export class EchoRequester extends Requester { - constructor(private status = 200) { - super(); - } - - send( - { headers, url, connectTimeout, responseTimeout }: EndRequest, - { data, ...originalRequest }: Request - ): Promise { - const urlSearchParams = new URL(url).searchParams; - const userAgent = urlSearchParams.get('x-algolia-agent') || undefined; - const originalData = - data && Object.entries(data).length > 0 ? data : undefined; - - return Promise.resolve({ - content: JSON.stringify({ - ...originalRequest, - headers, - connectTimeout, - responseTimeout, - userAgent: userAgent ? encodeURI(userAgent) : undefined, - searchParams: searchParamsWithoutUA(urlSearchParams), - data: originalData, - }), - isTimedOut: false, - status: this.status, - }); - } -} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/requester/HttpRequester.ts b/clients/algoliasearch-client-javascript/client-insights/utils/requester/HttpRequester.ts deleted file mode 100644 index 3697d290fb7..00000000000 --- a/clients/algoliasearch-client-javascript/client-insights/utils/requester/HttpRequester.ts +++ /dev/null @@ -1,94 +0,0 @@ -import http from 'http'; -import https from 'https'; - -import type { EndRequest, Response } from '../types'; - -import { Requester } from './Requester'; - -// Global agents allow us to reuse the TCP protocol with multiple clients -const agentOptions = { keepAlive: true }; -const httpAgent = new http.Agent(agentOptions); -const httpsAgent = new https.Agent(agentOptions); - -export class HttpRequester extends Requester { - send(request: EndRequest): Promise { - return new Promise((resolve) => { - let responseTimeout: NodeJS.Timeout | undefined; - // eslint-disable-next-line prefer-const -- linter thinks this is not reassigned - let connectTimeout: NodeJS.Timeout | undefined; - const url = new URL(request.url); - const path = - url.search === null ? url.pathname : `${url.pathname}${url.search}`; - const options: https.RequestOptions = { - agent: url.protocol === 'https:' ? httpsAgent : httpAgent, - hostname: url.hostname, - path, - method: request.method, - headers: request.headers, - ...(url.port !== undefined ? { port: url.port || '' } : {}), - }; - - const req = (url.protocol === 'https:' ? https : http).request( - options, - (response) => { - let contentBuffers: Buffer[] = []; - - response.on('data', (chunk) => { - contentBuffers = contentBuffers.concat(chunk); - }); - - response.on('end', () => { - clearTimeout(connectTimeout as NodeJS.Timeout); - clearTimeout(responseTimeout as NodeJS.Timeout); - - resolve({ - status: response.statusCode || 0, - content: Buffer.concat(contentBuffers).toString(), - isTimedOut: false, - }); - }); - } - ); - - const createTimeout = ( - timeout: number, - content: string - ): NodeJS.Timeout => { - return setTimeout(() => { - req.destroy(); - - resolve({ - status: 0, - content, - isTimedOut: true, - }); - }, timeout * 1000); - }; - - connectTimeout = createTimeout( - request.connectTimeout, - 'Connection timeout' - ); - - req.on('error', (error) => { - clearTimeout(connectTimeout as NodeJS.Timeout); - clearTimeout(responseTimeout!); - resolve({ status: 0, content: error.message, isTimedOut: false }); - }); - - req.once('response', () => { - clearTimeout(connectTimeout as NodeJS.Timeout); - responseTimeout = createTimeout( - request.responseTimeout, - 'Socket timeout' - ); - }); - - if (request.data !== undefined) { - req.write(request.data); - } - - req.end(); - }); - } -} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/requester/Requester.ts b/clients/algoliasearch-client-javascript/client-insights/utils/requester/Requester.ts deleted file mode 100644 index 41c06065753..00000000000 --- a/clients/algoliasearch-client-javascript/client-insights/utils/requester/Requester.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { EndRequest, Request, Response } from '../types'; - -export abstract class Requester { - abstract send( - request: EndRequest, - originalRequest: Request - ): Promise; -} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/stackTrace.ts b/clients/algoliasearch-client-javascript/client-insights/utils/stackTrace.ts deleted file mode 100644 index 14750a54f22..00000000000 --- a/clients/algoliasearch-client-javascript/client-insights/utils/stackTrace.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { StackFrame } from './types'; - -export function stackTraceWithoutCredentials( - stackTrace: StackFrame[] -): StackFrame[] { - return stackTrace.map((stackFrame) => - stackFrameWithoutCredentials(stackFrame) - ); -} - -export function stackFrameWithoutCredentials( - stackFrame: StackFrame -): StackFrame { - const modifiedHeaders: Record = stackFrame.request.headers[ - 'x-algolia-api-key' - ] - ? { 'x-algolia-api-key': '*****' } - : {}; - - return { - ...stackFrame, - request: { - ...stackFrame.request, - headers: { - ...stackFrame.request.headers, - ...modifiedHeaders, - }, - }, - }; -} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/types.ts b/clients/algoliasearch-client-javascript/client-insights/utils/types.ts deleted file mode 100644 index 20136fe367b..00000000000 --- a/clients/algoliasearch-client-javascript/client-insights/utils/types.ts +++ /dev/null @@ -1,73 +0,0 @@ -export type Method = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT'; - -export type Request = { - method: Method; - path: string; - data?: Record; -}; - -export type RequestOptions = { - /** - * Custom timeout for the request. Note that, in normal situacions - * the given timeout will be applied. But the transporter layer may - * increase this timeout if there is need for it. - */ - timeout?: number; - - /** - * Custom headers for the request. This headers are - * going to be merged the transporter headers. - */ - headers?: Record; - - /** - * Custom query parameters for the request. This query parameters are - * going to be merged the transporter query parameters. - */ - queryParameters: Record; - data?: Record; -}; - -export type EndRequest = { - method: Method; - url: string; - connectTimeout: number; - responseTimeout: number; - headers: Headers; - data?: string; -}; - -export type Response = { - content: string; - isTimedOut: boolean; - status: number; -}; - -export type EchoResponse = Request & { - connectTimeout: number; - headers: Record; - responseTimeout: number; - searchParams?: Record; - userAgent?: string; -}; - -export type Headers = Record; - -export type Host = { - url: string; - accept: 'read' | 'readWrite' | 'write'; - protocol: 'http' | 'https'; -}; - -export type StackFrame = { - request: EndRequest; - response: Response; - host: Host; - triesLeft: number; -}; - -export type Timeouts = { - readonly connect: number; - readonly read: number; - readonly write: number; -}; diff --git a/clients/algoliasearch-client-javascript/client-personalization/package.json b/clients/algoliasearch-client-javascript/client-personalization/package.json index 91d1427406f..e0a18ed0441 100644 --- a/clients/algoliasearch-client-javascript/client-personalization/package.json +++ b/clients/algoliasearch-client-javascript/client-personalization/package.json @@ -9,14 +9,16 @@ "main": "dist/api.js", "types": "dist/api.d.ts", "scripts": { - "clean": "rm -Rf node_modules/ *.js", "build": "tsc", - "test": "yarn build && node dist/client.js" + "clean": "rm -rf dist/" }, "engines": { "node": "^16.0.0", "yarn": "^3.0.0" }, + "dependencies": { + "@algolia/client-common": "5.0.0" + }, "devDependencies": { "@types/node": "16.11.11", "typescript": "4.5.4" diff --git a/clients/algoliasearch-client-javascript/client-personalization/src/apis.ts b/clients/algoliasearch-client-javascript/client-personalization/src/apis.ts index f4e5008c297..e4883c1626b 100644 --- a/clients/algoliasearch-client-javascript/client-personalization/src/apis.ts +++ b/clients/algoliasearch-client-javascript/client-personalization/src/apis.ts @@ -1,8 +1,6 @@ import { PersonalizationApi } from './personalizationApi'; export * from './personalizationApi'; -export * from '../utils/errors'; -export { EchoRequester } from '../utils/requester/EchoRequester'; -export { EchoResponse } from '../utils/types'; +export * from '@algolia/client-common'; export const APIS = [PersonalizationApi]; diff --git a/clients/algoliasearch-client-javascript/client-personalization/src/personalizationApi.ts b/clients/algoliasearch-client-javascript/client-personalization/src/personalizationApi.ts index 6ee3d2aaacc..079eaa354d4 100644 --- a/clients/algoliasearch-client-javascript/client-personalization/src/personalizationApi.ts +++ b/clients/algoliasearch-client-javascript/client-personalization/src/personalizationApi.ts @@ -1,10 +1,16 @@ +import { Transporter } from '@algolia/client-common'; +import type { + Headers, + Requester, + Host, + Request, + RequestOptions, +} from '@algolia/client-common'; + import type { DeleteUserProfileResponse } from '../model/deleteUserProfileResponse'; import type { GetUserTokenResponse } from '../model/getUserTokenResponse'; import type { PersonalizationStrategyParams } from '../model/personalizationStrategyParams'; import type { SetPersonalizationStrategyResponse } from '../model/setPersonalizationStrategyResponse'; -import { Transporter } from '../utils/Transporter'; -import type { Requester } from '../utils/requester/Requester'; -import type { Headers, Host, Request, RequestOptions } from '../utils/types'; export class PersonalizationApi { protected authentications = { diff --git a/clients/algoliasearch-client-javascript/client-personalization/utils/Response.ts b/clients/algoliasearch-client-javascript/client-personalization/utils/Response.ts deleted file mode 100644 index bd22de7df9e..00000000000 --- a/clients/algoliasearch-client-javascript/client-personalization/utils/Response.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Response } from './types'; - -export function isNetworkError({ - isTimedOut, - status, -}: Omit): boolean { - return !isTimedOut && ~~status === 0; -} - -export function isRetryable({ - isTimedOut, - status, -}: Omit): boolean { - return ( - isTimedOut || - isNetworkError({ isTimedOut, status }) || - (~~(status / 100) !== 2 && ~~(status / 100) !== 4) - ); -} - -export function isSuccess({ status }: Pick): boolean { - return ~~(status / 100) === 2; -} diff --git a/clients/algoliasearch-client-javascript/client-personalization/utils/StatefulHost.ts b/clients/algoliasearch-client-javascript/client-personalization/utils/StatefulHost.ts deleted file mode 100644 index 162c4ed1c66..00000000000 --- a/clients/algoliasearch-client-javascript/client-personalization/utils/StatefulHost.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Host } from './types'; - -const EXPIRATION_DELAY = 2 * 60 * 1000; - -export class StatefulHost implements Host { - url: string; - accept: 'read' | 'readWrite' | 'write'; - protocol: 'http' | 'https'; - - private lastUpdate: number; - private status: 'down' | 'timedout' | 'up'; - - constructor(host: Host, status: StatefulHost['status'] = 'up') { - this.url = host.url; - this.accept = host.accept; - this.protocol = host.protocol; - - this.status = status; - this.lastUpdate = Date.now(); - } - - isUp(): boolean { - return ( - this.status === 'up' || Date.now() - this.lastUpdate > EXPIRATION_DELAY - ); - } - - isTimedout(): boolean { - return ( - this.status === 'timedout' && - Date.now() - this.lastUpdate <= EXPIRATION_DELAY - ); - } -} diff --git a/clients/algoliasearch-client-javascript/client-personalization/utils/Transporter.ts b/clients/algoliasearch-client-javascript/client-personalization/utils/Transporter.ts deleted file mode 100644 index 48b4edebfdb..00000000000 --- a/clients/algoliasearch-client-javascript/client-personalization/utils/Transporter.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { isRetryable, isSuccess } from './Response'; -import { StatefulHost } from './StatefulHost'; -import type { Cache } from './cache/Cache'; -import { MemoryCache } from './cache/MemoryCache'; -import { RetryError } from './errors'; -import { - deserializeFailure, - deserializeSuccess, - serializeData, - serializeHeaders, - serializeUrl, -} from './helpers'; -import { HttpRequester } from './requester/HttpRequester'; -import type { Requester } from './requester/Requester'; -import { - stackTraceWithoutCredentials, - stackFrameWithoutCredentials, -} from './stackTrace'; -import type { - Headers, - Host, - Request, - RequestOptions, - StackFrame, - Timeouts, - Response, - EndRequest, -} from './types'; - -export class Transporter { - private hosts: Host[]; - private baseHeaders: Headers; - private hostsCache: Cache; - private userAgent: string; - private timeouts: Timeouts; - private requester: Requester; - - constructor({ - hosts, - baseHeaders, - userAgent, - timeouts, - requester = new HttpRequester(), - }: { - hosts: Host[]; - baseHeaders: Headers; - userAgent: string; - timeouts: Timeouts; - requester?: Requester; - }) { - this.hosts = hosts; - this.hostsCache = new MemoryCache(); - this.baseHeaders = baseHeaders; - this.userAgent = userAgent; - this.timeouts = timeouts; - this.requester = requester; - } - - setHosts(hosts: Host[]): void { - this.hosts = hosts; - this.hostsCache.clear(); - } - - setRequester(requester: Requester): void { - this.requester = requester; - } - - async createRetryableOptions(compatibleHosts: Host[]): Promise<{ - hosts: Host[]; - getTimeout: (retryCount: number, timeout: number) => number; - }> { - const statefulHosts = await Promise.all( - compatibleHosts.map((statelessHost) => { - return this.hostsCache.get(statelessHost, () => { - return Promise.resolve(new StatefulHost(statelessHost)); - }); - }) - ); - const hostsUp = statefulHosts.filter((host) => host.isUp()); - const hostsTimeouted = statefulHosts.filter((host) => host.isTimedout()); - - /** - * Note, we put the hosts that previously timeouted on the end of the list. - */ - const hostsAvailable = [...hostsUp, ...hostsTimeouted]; - - const hosts = hostsAvailable.length > 0 ? hostsAvailable : compatibleHosts; - - return { - hosts, - getTimeout(timeoutsCount: number, baseTimeout: number): number { - /** - * Imagine that you have 4 hosts, if timeouts will increase - * on the following way: 1 (timeouted) > 4 (timeouted) > 5 (200). - * - * Note that, the very next request, we start from the previous timeout. - * - * 5 (timeouted) > 6 (timeouted) > 7 ... - * - * This strategy may need to be reviewed, but is the strategy on the our - * current v3 version. - */ - const timeoutMultiplier = - hostsTimeouted.length === 0 && timeoutsCount === 0 - ? 1 - : hostsTimeouted.length + 3 + timeoutsCount; - - return timeoutMultiplier * baseTimeout; - }, - }; - } - - async request( - request: Request, - requestOptions: RequestOptions - ): Promise { - const stackTrace: StackFrame[] = []; - - const isRead = request.method === 'GET'; - - /** - * First we prepare the payload that do not depend from hosts. - */ - const data = serializeData(request, requestOptions); - const headers = serializeHeaders(this.baseHeaders, requestOptions); - const method = request.method; - - // On `GET`, the data is proxied to query parameters. - const dataQueryParameters: Record = isRead - ? { - ...request.data, - ...requestOptions.data, - } - : {}; - - const queryParameters = { - 'x-algolia-agent': this.userAgent, - ...dataQueryParameters, - ...requestOptions.queryParameters, - }; - - let timeoutsCount = 0; - - const retry = async ( - hosts: Host[], - getTimeout: (timeoutsCount: number, timeout: number) => number - ): Promise => { - /** - * We iterate on each host, until there is no host left. - */ - const host = hosts.pop(); - if (host === undefined) { - throw new RetryError(stackTraceWithoutCredentials(stackTrace)); - } - - let responseTimeout = requestOptions.timeout; - if (responseTimeout === undefined) { - responseTimeout = isRead ? this.timeouts.read : this.timeouts.write; - } - - const payload: EndRequest = { - data, - headers, - method, - url: serializeUrl(host, request.path, queryParameters), - connectTimeout: getTimeout(timeoutsCount, this.timeouts.connect), - responseTimeout: getTimeout(timeoutsCount, responseTimeout), - }; - - /** - * The stackFrame is pushed to the stackTrace so we - * can have information about onRetry and onFailure - * decisions. - */ - const pushToStackTrace = (response: Response): StackFrame => { - const stackFrame: StackFrame = { - request: payload, - response, - host, - triesLeft: hosts.length, - }; - - stackTrace.push(stackFrame); - - return stackFrame; - }; - - const response = await this.requester.send(payload, request); - - if (isRetryable(response)) { - const stackFrame = pushToStackTrace(response); - - // If response is a timeout, we increase the number of timeouts so we can increase the timeout later. - if (response.isTimedOut) { - timeoutsCount++; - } - /** - * Failures are individually sent to the logger, allowing - * the end user to debug / store stack frames even - * when a retry error does not happen. - */ - // eslint-disable-next-line no-console -- this will be fixed with the new `Logger` - console.log( - 'Retryable failure', - stackFrameWithoutCredentials(stackFrame) - ); - - /** - * We also store the state of the host in failure cases. If the host, is - * down it will remain down for the next 2 minutes. In a timeout situation, - * this host will be added end of the list of hosts on the next request. - */ - await this.hostsCache.set( - host, - new StatefulHost(host, response.isTimedOut ? 'timedout' : 'down') - ); - return retry(hosts, getTimeout); - } - if (isSuccess(response)) { - return deserializeSuccess(response); - } - - pushToStackTrace(response); - throw deserializeFailure(response, stackTrace); - }; - - /** - * Finally, for each retryable host perform request until we got a non - * retryable response. Some notes here: - * - * 1. The reverse here is applied so we can apply a `pop` later on => more performant. - * 2. We also get from the retryable options a timeout multiplier that is tailored - * for the current context. - */ - const compatibleHosts = this.hosts.filter( - (host) => - host.accept === 'readWrite' || - (isRead ? host.accept === 'read' : host.accept === 'write') - ); - const options = await this.createRetryableOptions(compatibleHosts); - return retry([...options.hosts].reverse(), options.getTimeout); - } -} diff --git a/clients/algoliasearch-client-javascript/client-personalization/utils/cache/Cache.ts b/clients/algoliasearch-client-javascript/client-personalization/utils/cache/Cache.ts deleted file mode 100644 index 625862660c6..00000000000 --- a/clients/algoliasearch-client-javascript/client-personalization/utils/cache/Cache.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface Cache { - /** - * Gets the value of the given `key`. - */ - get: ( - key: Record | string, - defaultValue: () => Promise - ) => Promise; - - /** - * Sets the given value with the given `key`. - */ - set: ( - key: Record | string, - value: TValue - ) => Promise; - - /** - * Deletes the given `key`. - */ - delete: (key: Record | string) => Promise; - - /** - * Clears the cache. - */ - clear: () => Promise; -} diff --git a/clients/algoliasearch-client-javascript/client-personalization/utils/cache/MemoryCache.ts b/clients/algoliasearch-client-javascript/client-personalization/utils/cache/MemoryCache.ts deleted file mode 100644 index f7853f39bc4..00000000000 --- a/clients/algoliasearch-client-javascript/client-personalization/utils/cache/MemoryCache.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Cache } from './Cache'; - -export class MemoryCache implements Cache { - private cache: Record = {}; - - async get( - key: Record | string, - defaultValue: () => Promise - ): Promise { - const keyAsString = JSON.stringify(key); - - if (keyAsString in this.cache) { - return Promise.resolve(this.cache[keyAsString]); - } - - return await defaultValue(); - } - - set( - key: Record | string, - value: TValue - ): Promise { - this.cache[JSON.stringify(key)] = value; - - return Promise.resolve(value); - } - - delete(key: Record | string): Promise { - delete this.cache[JSON.stringify(key)]; - - return Promise.resolve(); - } - - clear(): Promise { - this.cache = {}; - - return Promise.resolve(); - } -} diff --git a/clients/algoliasearch-client-javascript/client-personalization/utils/errors.ts b/clients/algoliasearch-client-javascript/client-personalization/utils/errors.ts deleted file mode 100644 index a02f3004ad6..00000000000 --- a/clients/algoliasearch-client-javascript/client-personalization/utils/errors.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Response, StackFrame } from './types'; - -class ErrorWithStackTrace extends Error { - stackTrace: StackFrame[]; - - constructor(message: string, stackTrace: StackFrame[]) { - super(message); - // the array and object should be frozen to reflect the stackTrace at the time of the error - this.stackTrace = stackTrace; - } -} - -export class RetryError extends ErrorWithStackTrace { - constructor(stackTrace: StackFrame[]) { - super( - 'Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.', - stackTrace - ); - } -} - -export class ApiError extends ErrorWithStackTrace { - status: number; - - constructor(message: string, status: number, stackTrace: StackFrame[]) { - super(message, stackTrace); - this.status = status; - } -} - -export class DeserializationError extends Error { - response: Response; - - constructor(message: string, response: Response) { - super(message); - this.response = response; - } -} diff --git a/clients/algoliasearch-client-javascript/client-personalization/utils/helpers.ts b/clients/algoliasearch-client-javascript/client-personalization/utils/helpers.ts deleted file mode 100644 index 5d64ac88688..00000000000 --- a/clients/algoliasearch-client-javascript/client-personalization/utils/helpers.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { ApiError, DeserializationError } from './errors'; -import type { - Headers, - Host, - Request, - RequestOptions, - Response, - StackFrame, -} from './types'; - -export function shuffle(array: TData[]): TData[] { - const shuffledArray = array; - - for (let c = array.length - 1; c > 0; c--) { - const b = Math.floor(Math.random() * (c + 1)); - const a = array[c]; - - shuffledArray[c] = array[b]; - shuffledArray[b] = a; - } - - return shuffledArray; -} - -export function serializeUrl( - host: Host, - path: string, - queryParameters: Readonly> -): string { - const queryParametersAsString = serializeQueryParameters(queryParameters); - let url = `${host.protocol}://${host.url}/${ - path.charAt(0) === '/' ? path.substr(1) : path - }`; - - if (queryParametersAsString.length) { - url += `?${queryParametersAsString}`; - } - - return url; -} - -export function serializeQueryParameters( - parameters: Readonly> -): string { - const isObjectOrArray = (value: any): boolean => - Object.prototype.toString.call(value) === '[object Object]' || - Object.prototype.toString.call(value) === '[object Array]'; - - return Object.keys(parameters) - .map( - (key) => - `${key}=${ - isObjectOrArray(parameters[key]) - ? JSON.stringify(parameters[key]) - : parameters[key] - }` - ) - .join('&'); -} - -export function serializeData( - request: Request, - requestOptions: RequestOptions -): string | undefined { - if ( - request.method === 'GET' || - (request.data === undefined && requestOptions.data === undefined) - ) { - return undefined; - } - - const data = Array.isArray(request.data) - ? request.data - : { ...request.data, ...requestOptions.data }; - - return JSON.stringify(data); -} - -export function serializeHeaders( - baseHeaders: Headers, - requestOptions: RequestOptions -): Headers { - const headers: Headers = { - ...baseHeaders, - ...requestOptions.headers, - }; - const serializedHeaders: Headers = {}; - - Object.keys(headers).forEach((header) => { - const value = headers[header]; - serializedHeaders[header.toLowerCase()] = value; - }); - - return serializedHeaders; -} - -export function deserializeSuccess(response: Response): TObject { - try { - return JSON.parse(response.content); - } catch (e) { - throw new DeserializationError((e as Error).message, response); - } -} - -export function deserializeFailure( - { content, status }: Response, - stackFrame: StackFrame[] -): Error { - let message = content; - try { - message = JSON.parse(content).message; - } catch (e) { - // .. - } - - return new ApiError(message, status, stackFrame); -} diff --git a/clients/algoliasearch-client-javascript/client-personalization/utils/requester/EchoRequester.ts b/clients/algoliasearch-client-javascript/client-personalization/utils/requester/EchoRequester.ts deleted file mode 100644 index f773d0f6379..00000000000 --- a/clients/algoliasearch-client-javascript/client-personalization/utils/requester/EchoRequester.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { EndRequest, Request, Response, EchoResponse } from '../types'; - -import { Requester } from './Requester'; - -function searchParamsWithoutUA( - params: URLSearchParams -): EchoResponse['searchParams'] { - const searchParams = {}; - - for (const [k, v] of params) { - if (k === 'x-algolia-agent') { - continue; - } - - searchParams[k] = v; - } - - return Object.entries(searchParams).length === 0 ? undefined : searchParams; -} - -export class EchoRequester extends Requester { - constructor(private status = 200) { - super(); - } - - send( - { headers, url, connectTimeout, responseTimeout }: EndRequest, - { data, ...originalRequest }: Request - ): Promise { - const urlSearchParams = new URL(url).searchParams; - const userAgent = urlSearchParams.get('x-algolia-agent') || undefined; - const originalData = - data && Object.entries(data).length > 0 ? data : undefined; - - return Promise.resolve({ - content: JSON.stringify({ - ...originalRequest, - headers, - connectTimeout, - responseTimeout, - userAgent: userAgent ? encodeURI(userAgent) : undefined, - searchParams: searchParamsWithoutUA(urlSearchParams), - data: originalData, - }), - isTimedOut: false, - status: this.status, - }); - } -} diff --git a/clients/algoliasearch-client-javascript/client-personalization/utils/requester/HttpRequester.ts b/clients/algoliasearch-client-javascript/client-personalization/utils/requester/HttpRequester.ts deleted file mode 100644 index 3697d290fb7..00000000000 --- a/clients/algoliasearch-client-javascript/client-personalization/utils/requester/HttpRequester.ts +++ /dev/null @@ -1,94 +0,0 @@ -import http from 'http'; -import https from 'https'; - -import type { EndRequest, Response } from '../types'; - -import { Requester } from './Requester'; - -// Global agents allow us to reuse the TCP protocol with multiple clients -const agentOptions = { keepAlive: true }; -const httpAgent = new http.Agent(agentOptions); -const httpsAgent = new https.Agent(agentOptions); - -export class HttpRequester extends Requester { - send(request: EndRequest): Promise { - return new Promise((resolve) => { - let responseTimeout: NodeJS.Timeout | undefined; - // eslint-disable-next-line prefer-const -- linter thinks this is not reassigned - let connectTimeout: NodeJS.Timeout | undefined; - const url = new URL(request.url); - const path = - url.search === null ? url.pathname : `${url.pathname}${url.search}`; - const options: https.RequestOptions = { - agent: url.protocol === 'https:' ? httpsAgent : httpAgent, - hostname: url.hostname, - path, - method: request.method, - headers: request.headers, - ...(url.port !== undefined ? { port: url.port || '' } : {}), - }; - - const req = (url.protocol === 'https:' ? https : http).request( - options, - (response) => { - let contentBuffers: Buffer[] = []; - - response.on('data', (chunk) => { - contentBuffers = contentBuffers.concat(chunk); - }); - - response.on('end', () => { - clearTimeout(connectTimeout as NodeJS.Timeout); - clearTimeout(responseTimeout as NodeJS.Timeout); - - resolve({ - status: response.statusCode || 0, - content: Buffer.concat(contentBuffers).toString(), - isTimedOut: false, - }); - }); - } - ); - - const createTimeout = ( - timeout: number, - content: string - ): NodeJS.Timeout => { - return setTimeout(() => { - req.destroy(); - - resolve({ - status: 0, - content, - isTimedOut: true, - }); - }, timeout * 1000); - }; - - connectTimeout = createTimeout( - request.connectTimeout, - 'Connection timeout' - ); - - req.on('error', (error) => { - clearTimeout(connectTimeout as NodeJS.Timeout); - clearTimeout(responseTimeout!); - resolve({ status: 0, content: error.message, isTimedOut: false }); - }); - - req.once('response', () => { - clearTimeout(connectTimeout as NodeJS.Timeout); - responseTimeout = createTimeout( - request.responseTimeout, - 'Socket timeout' - ); - }); - - if (request.data !== undefined) { - req.write(request.data); - } - - req.end(); - }); - } -} diff --git a/clients/algoliasearch-client-javascript/client-personalization/utils/requester/Requester.ts b/clients/algoliasearch-client-javascript/client-personalization/utils/requester/Requester.ts deleted file mode 100644 index 41c06065753..00000000000 --- a/clients/algoliasearch-client-javascript/client-personalization/utils/requester/Requester.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { EndRequest, Request, Response } from '../types'; - -export abstract class Requester { - abstract send( - request: EndRequest, - originalRequest: Request - ): Promise; -} diff --git a/clients/algoliasearch-client-javascript/client-personalization/utils/stackTrace.ts b/clients/algoliasearch-client-javascript/client-personalization/utils/stackTrace.ts deleted file mode 100644 index 14750a54f22..00000000000 --- a/clients/algoliasearch-client-javascript/client-personalization/utils/stackTrace.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { StackFrame } from './types'; - -export function stackTraceWithoutCredentials( - stackTrace: StackFrame[] -): StackFrame[] { - return stackTrace.map((stackFrame) => - stackFrameWithoutCredentials(stackFrame) - ); -} - -export function stackFrameWithoutCredentials( - stackFrame: StackFrame -): StackFrame { - const modifiedHeaders: Record = stackFrame.request.headers[ - 'x-algolia-api-key' - ] - ? { 'x-algolia-api-key': '*****' } - : {}; - - return { - ...stackFrame, - request: { - ...stackFrame.request, - headers: { - ...stackFrame.request.headers, - ...modifiedHeaders, - }, - }, - }; -} diff --git a/clients/algoliasearch-client-javascript/client-personalization/utils/types.ts b/clients/algoliasearch-client-javascript/client-personalization/utils/types.ts deleted file mode 100644 index 20136fe367b..00000000000 --- a/clients/algoliasearch-client-javascript/client-personalization/utils/types.ts +++ /dev/null @@ -1,73 +0,0 @@ -export type Method = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT'; - -export type Request = { - method: Method; - path: string; - data?: Record; -}; - -export type RequestOptions = { - /** - * Custom timeout for the request. Note that, in normal situacions - * the given timeout will be applied. But the transporter layer may - * increase this timeout if there is need for it. - */ - timeout?: number; - - /** - * Custom headers for the request. This headers are - * going to be merged the transporter headers. - */ - headers?: Record; - - /** - * Custom query parameters for the request. This query parameters are - * going to be merged the transporter query parameters. - */ - queryParameters: Record; - data?: Record; -}; - -export type EndRequest = { - method: Method; - url: string; - connectTimeout: number; - responseTimeout: number; - headers: Headers; - data?: string; -}; - -export type Response = { - content: string; - isTimedOut: boolean; - status: number; -}; - -export type EchoResponse = Request & { - connectTimeout: number; - headers: Record; - responseTimeout: number; - searchParams?: Record; - userAgent?: string; -}; - -export type Headers = Record; - -export type Host = { - url: string; - accept: 'read' | 'readWrite' | 'write'; - protocol: 'http' | 'https'; -}; - -export type StackFrame = { - request: EndRequest; - response: Response; - host: Host; - triesLeft: number; -}; - -export type Timeouts = { - readonly connect: number; - readonly read: number; - readonly write: number; -}; diff --git a/clients/algoliasearch-client-javascript/client-query-suggestions/package.json b/clients/algoliasearch-client-javascript/client-query-suggestions/package.json index 680f2261003..422db8a534d 100644 --- a/clients/algoliasearch-client-javascript/client-query-suggestions/package.json +++ b/clients/algoliasearch-client-javascript/client-query-suggestions/package.json @@ -9,14 +9,16 @@ "main": "dist/api.js", "types": "dist/api.d.ts", "scripts": { - "clean": "rm -Rf node_modules/ *.js", "build": "tsc", - "test": "yarn build && node dist/client.js" + "clean": "rm -rf dist/" }, "engines": { "node": "^16.0.0", "yarn": "^3.0.0" }, + "dependencies": { + "@algolia/client-common": "5.0.0" + }, "devDependencies": { "@types/node": "16.11.11", "typescript": "4.5.4" diff --git a/clients/algoliasearch-client-javascript/client-query-suggestions/src/apis.ts b/clients/algoliasearch-client-javascript/client-query-suggestions/src/apis.ts index 4127dccd9ca..cff36ebfb20 100644 --- a/clients/algoliasearch-client-javascript/client-query-suggestions/src/apis.ts +++ b/clients/algoliasearch-client-javascript/client-query-suggestions/src/apis.ts @@ -1,8 +1,6 @@ import { QuerySuggestionsApi } from './querySuggestionsApi'; export * from './querySuggestionsApi'; -export * from '../utils/errors'; -export { EchoRequester } from '../utils/requester/EchoRequester'; -export { EchoResponse } from '../utils/types'; +export * from '@algolia/client-common'; export const APIS = [QuerySuggestionsApi]; diff --git a/clients/algoliasearch-client-javascript/client-query-suggestions/src/querySuggestionsApi.ts b/clients/algoliasearch-client-javascript/client-query-suggestions/src/querySuggestionsApi.ts index ea0c3c9e10b..859e6636d8d 100644 --- a/clients/algoliasearch-client-javascript/client-query-suggestions/src/querySuggestionsApi.ts +++ b/clients/algoliasearch-client-javascript/client-query-suggestions/src/querySuggestionsApi.ts @@ -1,12 +1,18 @@ +import { Transporter } from '@algolia/client-common'; +import type { + Headers, + Requester, + Host, + Request, + RequestOptions, +} from '@algolia/client-common'; + import type { LogFile } from '../model/logFile'; import type { QuerySuggestionsIndex } from '../model/querySuggestionsIndex'; import type { QuerySuggestionsIndexParam } from '../model/querySuggestionsIndexParam'; import type { QuerySuggestionsIndexWithIndexParam } from '../model/querySuggestionsIndexWithIndexParam'; import type { Status } from '../model/status'; import type { SucessResponse } from '../model/sucessResponse'; -import { Transporter } from '../utils/Transporter'; -import type { Requester } from '../utils/requester/Requester'; -import type { Headers, Host, Request, RequestOptions } from '../utils/types'; export class QuerySuggestionsApi { protected authentications = { diff --git a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/Response.ts b/clients/algoliasearch-client-javascript/client-query-suggestions/utils/Response.ts deleted file mode 100644 index bd22de7df9e..00000000000 --- a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/Response.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Response } from './types'; - -export function isNetworkError({ - isTimedOut, - status, -}: Omit): boolean { - return !isTimedOut && ~~status === 0; -} - -export function isRetryable({ - isTimedOut, - status, -}: Omit): boolean { - return ( - isTimedOut || - isNetworkError({ isTimedOut, status }) || - (~~(status / 100) !== 2 && ~~(status / 100) !== 4) - ); -} - -export function isSuccess({ status }: Pick): boolean { - return ~~(status / 100) === 2; -} diff --git a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/StatefulHost.ts b/clients/algoliasearch-client-javascript/client-query-suggestions/utils/StatefulHost.ts deleted file mode 100644 index 162c4ed1c66..00000000000 --- a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/StatefulHost.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Host } from './types'; - -const EXPIRATION_DELAY = 2 * 60 * 1000; - -export class StatefulHost implements Host { - url: string; - accept: 'read' | 'readWrite' | 'write'; - protocol: 'http' | 'https'; - - private lastUpdate: number; - private status: 'down' | 'timedout' | 'up'; - - constructor(host: Host, status: StatefulHost['status'] = 'up') { - this.url = host.url; - this.accept = host.accept; - this.protocol = host.protocol; - - this.status = status; - this.lastUpdate = Date.now(); - } - - isUp(): boolean { - return ( - this.status === 'up' || Date.now() - this.lastUpdate > EXPIRATION_DELAY - ); - } - - isTimedout(): boolean { - return ( - this.status === 'timedout' && - Date.now() - this.lastUpdate <= EXPIRATION_DELAY - ); - } -} diff --git a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/Transporter.ts b/clients/algoliasearch-client-javascript/client-query-suggestions/utils/Transporter.ts deleted file mode 100644 index 48b4edebfdb..00000000000 --- a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/Transporter.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { isRetryable, isSuccess } from './Response'; -import { StatefulHost } from './StatefulHost'; -import type { Cache } from './cache/Cache'; -import { MemoryCache } from './cache/MemoryCache'; -import { RetryError } from './errors'; -import { - deserializeFailure, - deserializeSuccess, - serializeData, - serializeHeaders, - serializeUrl, -} from './helpers'; -import { HttpRequester } from './requester/HttpRequester'; -import type { Requester } from './requester/Requester'; -import { - stackTraceWithoutCredentials, - stackFrameWithoutCredentials, -} from './stackTrace'; -import type { - Headers, - Host, - Request, - RequestOptions, - StackFrame, - Timeouts, - Response, - EndRequest, -} from './types'; - -export class Transporter { - private hosts: Host[]; - private baseHeaders: Headers; - private hostsCache: Cache; - private userAgent: string; - private timeouts: Timeouts; - private requester: Requester; - - constructor({ - hosts, - baseHeaders, - userAgent, - timeouts, - requester = new HttpRequester(), - }: { - hosts: Host[]; - baseHeaders: Headers; - userAgent: string; - timeouts: Timeouts; - requester?: Requester; - }) { - this.hosts = hosts; - this.hostsCache = new MemoryCache(); - this.baseHeaders = baseHeaders; - this.userAgent = userAgent; - this.timeouts = timeouts; - this.requester = requester; - } - - setHosts(hosts: Host[]): void { - this.hosts = hosts; - this.hostsCache.clear(); - } - - setRequester(requester: Requester): void { - this.requester = requester; - } - - async createRetryableOptions(compatibleHosts: Host[]): Promise<{ - hosts: Host[]; - getTimeout: (retryCount: number, timeout: number) => number; - }> { - const statefulHosts = await Promise.all( - compatibleHosts.map((statelessHost) => { - return this.hostsCache.get(statelessHost, () => { - return Promise.resolve(new StatefulHost(statelessHost)); - }); - }) - ); - const hostsUp = statefulHosts.filter((host) => host.isUp()); - const hostsTimeouted = statefulHosts.filter((host) => host.isTimedout()); - - /** - * Note, we put the hosts that previously timeouted on the end of the list. - */ - const hostsAvailable = [...hostsUp, ...hostsTimeouted]; - - const hosts = hostsAvailable.length > 0 ? hostsAvailable : compatibleHosts; - - return { - hosts, - getTimeout(timeoutsCount: number, baseTimeout: number): number { - /** - * Imagine that you have 4 hosts, if timeouts will increase - * on the following way: 1 (timeouted) > 4 (timeouted) > 5 (200). - * - * Note that, the very next request, we start from the previous timeout. - * - * 5 (timeouted) > 6 (timeouted) > 7 ... - * - * This strategy may need to be reviewed, but is the strategy on the our - * current v3 version. - */ - const timeoutMultiplier = - hostsTimeouted.length === 0 && timeoutsCount === 0 - ? 1 - : hostsTimeouted.length + 3 + timeoutsCount; - - return timeoutMultiplier * baseTimeout; - }, - }; - } - - async request( - request: Request, - requestOptions: RequestOptions - ): Promise { - const stackTrace: StackFrame[] = []; - - const isRead = request.method === 'GET'; - - /** - * First we prepare the payload that do not depend from hosts. - */ - const data = serializeData(request, requestOptions); - const headers = serializeHeaders(this.baseHeaders, requestOptions); - const method = request.method; - - // On `GET`, the data is proxied to query parameters. - const dataQueryParameters: Record = isRead - ? { - ...request.data, - ...requestOptions.data, - } - : {}; - - const queryParameters = { - 'x-algolia-agent': this.userAgent, - ...dataQueryParameters, - ...requestOptions.queryParameters, - }; - - let timeoutsCount = 0; - - const retry = async ( - hosts: Host[], - getTimeout: (timeoutsCount: number, timeout: number) => number - ): Promise => { - /** - * We iterate on each host, until there is no host left. - */ - const host = hosts.pop(); - if (host === undefined) { - throw new RetryError(stackTraceWithoutCredentials(stackTrace)); - } - - let responseTimeout = requestOptions.timeout; - if (responseTimeout === undefined) { - responseTimeout = isRead ? this.timeouts.read : this.timeouts.write; - } - - const payload: EndRequest = { - data, - headers, - method, - url: serializeUrl(host, request.path, queryParameters), - connectTimeout: getTimeout(timeoutsCount, this.timeouts.connect), - responseTimeout: getTimeout(timeoutsCount, responseTimeout), - }; - - /** - * The stackFrame is pushed to the stackTrace so we - * can have information about onRetry and onFailure - * decisions. - */ - const pushToStackTrace = (response: Response): StackFrame => { - const stackFrame: StackFrame = { - request: payload, - response, - host, - triesLeft: hosts.length, - }; - - stackTrace.push(stackFrame); - - return stackFrame; - }; - - const response = await this.requester.send(payload, request); - - if (isRetryable(response)) { - const stackFrame = pushToStackTrace(response); - - // If response is a timeout, we increase the number of timeouts so we can increase the timeout later. - if (response.isTimedOut) { - timeoutsCount++; - } - /** - * Failures are individually sent to the logger, allowing - * the end user to debug / store stack frames even - * when a retry error does not happen. - */ - // eslint-disable-next-line no-console -- this will be fixed with the new `Logger` - console.log( - 'Retryable failure', - stackFrameWithoutCredentials(stackFrame) - ); - - /** - * We also store the state of the host in failure cases. If the host, is - * down it will remain down for the next 2 minutes. In a timeout situation, - * this host will be added end of the list of hosts on the next request. - */ - await this.hostsCache.set( - host, - new StatefulHost(host, response.isTimedOut ? 'timedout' : 'down') - ); - return retry(hosts, getTimeout); - } - if (isSuccess(response)) { - return deserializeSuccess(response); - } - - pushToStackTrace(response); - throw deserializeFailure(response, stackTrace); - }; - - /** - * Finally, for each retryable host perform request until we got a non - * retryable response. Some notes here: - * - * 1. The reverse here is applied so we can apply a `pop` later on => more performant. - * 2. We also get from the retryable options a timeout multiplier that is tailored - * for the current context. - */ - const compatibleHosts = this.hosts.filter( - (host) => - host.accept === 'readWrite' || - (isRead ? host.accept === 'read' : host.accept === 'write') - ); - const options = await this.createRetryableOptions(compatibleHosts); - return retry([...options.hosts].reverse(), options.getTimeout); - } -} diff --git a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/cache/Cache.ts b/clients/algoliasearch-client-javascript/client-query-suggestions/utils/cache/Cache.ts deleted file mode 100644 index 625862660c6..00000000000 --- a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/cache/Cache.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface Cache { - /** - * Gets the value of the given `key`. - */ - get: ( - key: Record | string, - defaultValue: () => Promise - ) => Promise; - - /** - * Sets the given value with the given `key`. - */ - set: ( - key: Record | string, - value: TValue - ) => Promise; - - /** - * Deletes the given `key`. - */ - delete: (key: Record | string) => Promise; - - /** - * Clears the cache. - */ - clear: () => Promise; -} diff --git a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/cache/MemoryCache.ts b/clients/algoliasearch-client-javascript/client-query-suggestions/utils/cache/MemoryCache.ts deleted file mode 100644 index f7853f39bc4..00000000000 --- a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/cache/MemoryCache.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Cache } from './Cache'; - -export class MemoryCache implements Cache { - private cache: Record = {}; - - async get( - key: Record | string, - defaultValue: () => Promise - ): Promise { - const keyAsString = JSON.stringify(key); - - if (keyAsString in this.cache) { - return Promise.resolve(this.cache[keyAsString]); - } - - return await defaultValue(); - } - - set( - key: Record | string, - value: TValue - ): Promise { - this.cache[JSON.stringify(key)] = value; - - return Promise.resolve(value); - } - - delete(key: Record | string): Promise { - delete this.cache[JSON.stringify(key)]; - - return Promise.resolve(); - } - - clear(): Promise { - this.cache = {}; - - return Promise.resolve(); - } -} diff --git a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/errors.ts b/clients/algoliasearch-client-javascript/client-query-suggestions/utils/errors.ts deleted file mode 100644 index a02f3004ad6..00000000000 --- a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/errors.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Response, StackFrame } from './types'; - -class ErrorWithStackTrace extends Error { - stackTrace: StackFrame[]; - - constructor(message: string, stackTrace: StackFrame[]) { - super(message); - // the array and object should be frozen to reflect the stackTrace at the time of the error - this.stackTrace = stackTrace; - } -} - -export class RetryError extends ErrorWithStackTrace { - constructor(stackTrace: StackFrame[]) { - super( - 'Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.', - stackTrace - ); - } -} - -export class ApiError extends ErrorWithStackTrace { - status: number; - - constructor(message: string, status: number, stackTrace: StackFrame[]) { - super(message, stackTrace); - this.status = status; - } -} - -export class DeserializationError extends Error { - response: Response; - - constructor(message: string, response: Response) { - super(message); - this.response = response; - } -} diff --git a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/helpers.ts b/clients/algoliasearch-client-javascript/client-query-suggestions/utils/helpers.ts deleted file mode 100644 index 5d64ac88688..00000000000 --- a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/helpers.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { ApiError, DeserializationError } from './errors'; -import type { - Headers, - Host, - Request, - RequestOptions, - Response, - StackFrame, -} from './types'; - -export function shuffle(array: TData[]): TData[] { - const shuffledArray = array; - - for (let c = array.length - 1; c > 0; c--) { - const b = Math.floor(Math.random() * (c + 1)); - const a = array[c]; - - shuffledArray[c] = array[b]; - shuffledArray[b] = a; - } - - return shuffledArray; -} - -export function serializeUrl( - host: Host, - path: string, - queryParameters: Readonly> -): string { - const queryParametersAsString = serializeQueryParameters(queryParameters); - let url = `${host.protocol}://${host.url}/${ - path.charAt(0) === '/' ? path.substr(1) : path - }`; - - if (queryParametersAsString.length) { - url += `?${queryParametersAsString}`; - } - - return url; -} - -export function serializeQueryParameters( - parameters: Readonly> -): string { - const isObjectOrArray = (value: any): boolean => - Object.prototype.toString.call(value) === '[object Object]' || - Object.prototype.toString.call(value) === '[object Array]'; - - return Object.keys(parameters) - .map( - (key) => - `${key}=${ - isObjectOrArray(parameters[key]) - ? JSON.stringify(parameters[key]) - : parameters[key] - }` - ) - .join('&'); -} - -export function serializeData( - request: Request, - requestOptions: RequestOptions -): string | undefined { - if ( - request.method === 'GET' || - (request.data === undefined && requestOptions.data === undefined) - ) { - return undefined; - } - - const data = Array.isArray(request.data) - ? request.data - : { ...request.data, ...requestOptions.data }; - - return JSON.stringify(data); -} - -export function serializeHeaders( - baseHeaders: Headers, - requestOptions: RequestOptions -): Headers { - const headers: Headers = { - ...baseHeaders, - ...requestOptions.headers, - }; - const serializedHeaders: Headers = {}; - - Object.keys(headers).forEach((header) => { - const value = headers[header]; - serializedHeaders[header.toLowerCase()] = value; - }); - - return serializedHeaders; -} - -export function deserializeSuccess(response: Response): TObject { - try { - return JSON.parse(response.content); - } catch (e) { - throw new DeserializationError((e as Error).message, response); - } -} - -export function deserializeFailure( - { content, status }: Response, - stackFrame: StackFrame[] -): Error { - let message = content; - try { - message = JSON.parse(content).message; - } catch (e) { - // .. - } - - return new ApiError(message, status, stackFrame); -} diff --git a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/requester/EchoRequester.ts b/clients/algoliasearch-client-javascript/client-query-suggestions/utils/requester/EchoRequester.ts deleted file mode 100644 index f773d0f6379..00000000000 --- a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/requester/EchoRequester.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { EndRequest, Request, Response, EchoResponse } from '../types'; - -import { Requester } from './Requester'; - -function searchParamsWithoutUA( - params: URLSearchParams -): EchoResponse['searchParams'] { - const searchParams = {}; - - for (const [k, v] of params) { - if (k === 'x-algolia-agent') { - continue; - } - - searchParams[k] = v; - } - - return Object.entries(searchParams).length === 0 ? undefined : searchParams; -} - -export class EchoRequester extends Requester { - constructor(private status = 200) { - super(); - } - - send( - { headers, url, connectTimeout, responseTimeout }: EndRequest, - { data, ...originalRequest }: Request - ): Promise { - const urlSearchParams = new URL(url).searchParams; - const userAgent = urlSearchParams.get('x-algolia-agent') || undefined; - const originalData = - data && Object.entries(data).length > 0 ? data : undefined; - - return Promise.resolve({ - content: JSON.stringify({ - ...originalRequest, - headers, - connectTimeout, - responseTimeout, - userAgent: userAgent ? encodeURI(userAgent) : undefined, - searchParams: searchParamsWithoutUA(urlSearchParams), - data: originalData, - }), - isTimedOut: false, - status: this.status, - }); - } -} diff --git a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/requester/HttpRequester.ts b/clients/algoliasearch-client-javascript/client-query-suggestions/utils/requester/HttpRequester.ts deleted file mode 100644 index 3697d290fb7..00000000000 --- a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/requester/HttpRequester.ts +++ /dev/null @@ -1,94 +0,0 @@ -import http from 'http'; -import https from 'https'; - -import type { EndRequest, Response } from '../types'; - -import { Requester } from './Requester'; - -// Global agents allow us to reuse the TCP protocol with multiple clients -const agentOptions = { keepAlive: true }; -const httpAgent = new http.Agent(agentOptions); -const httpsAgent = new https.Agent(agentOptions); - -export class HttpRequester extends Requester { - send(request: EndRequest): Promise { - return new Promise((resolve) => { - let responseTimeout: NodeJS.Timeout | undefined; - // eslint-disable-next-line prefer-const -- linter thinks this is not reassigned - let connectTimeout: NodeJS.Timeout | undefined; - const url = new URL(request.url); - const path = - url.search === null ? url.pathname : `${url.pathname}${url.search}`; - const options: https.RequestOptions = { - agent: url.protocol === 'https:' ? httpsAgent : httpAgent, - hostname: url.hostname, - path, - method: request.method, - headers: request.headers, - ...(url.port !== undefined ? { port: url.port || '' } : {}), - }; - - const req = (url.protocol === 'https:' ? https : http).request( - options, - (response) => { - let contentBuffers: Buffer[] = []; - - response.on('data', (chunk) => { - contentBuffers = contentBuffers.concat(chunk); - }); - - response.on('end', () => { - clearTimeout(connectTimeout as NodeJS.Timeout); - clearTimeout(responseTimeout as NodeJS.Timeout); - - resolve({ - status: response.statusCode || 0, - content: Buffer.concat(contentBuffers).toString(), - isTimedOut: false, - }); - }); - } - ); - - const createTimeout = ( - timeout: number, - content: string - ): NodeJS.Timeout => { - return setTimeout(() => { - req.destroy(); - - resolve({ - status: 0, - content, - isTimedOut: true, - }); - }, timeout * 1000); - }; - - connectTimeout = createTimeout( - request.connectTimeout, - 'Connection timeout' - ); - - req.on('error', (error) => { - clearTimeout(connectTimeout as NodeJS.Timeout); - clearTimeout(responseTimeout!); - resolve({ status: 0, content: error.message, isTimedOut: false }); - }); - - req.once('response', () => { - clearTimeout(connectTimeout as NodeJS.Timeout); - responseTimeout = createTimeout( - request.responseTimeout, - 'Socket timeout' - ); - }); - - if (request.data !== undefined) { - req.write(request.data); - } - - req.end(); - }); - } -} diff --git a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/requester/Requester.ts b/clients/algoliasearch-client-javascript/client-query-suggestions/utils/requester/Requester.ts deleted file mode 100644 index 41c06065753..00000000000 --- a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/requester/Requester.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { EndRequest, Request, Response } from '../types'; - -export abstract class Requester { - abstract send( - request: EndRequest, - originalRequest: Request - ): Promise; -} diff --git a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/stackTrace.ts b/clients/algoliasearch-client-javascript/client-query-suggestions/utils/stackTrace.ts deleted file mode 100644 index 14750a54f22..00000000000 --- a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/stackTrace.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { StackFrame } from './types'; - -export function stackTraceWithoutCredentials( - stackTrace: StackFrame[] -): StackFrame[] { - return stackTrace.map((stackFrame) => - stackFrameWithoutCredentials(stackFrame) - ); -} - -export function stackFrameWithoutCredentials( - stackFrame: StackFrame -): StackFrame { - const modifiedHeaders: Record = stackFrame.request.headers[ - 'x-algolia-api-key' - ] - ? { 'x-algolia-api-key': '*****' } - : {}; - - return { - ...stackFrame, - request: { - ...stackFrame.request, - headers: { - ...stackFrame.request.headers, - ...modifiedHeaders, - }, - }, - }; -} diff --git a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/types.ts b/clients/algoliasearch-client-javascript/client-query-suggestions/utils/types.ts deleted file mode 100644 index 20136fe367b..00000000000 --- a/clients/algoliasearch-client-javascript/client-query-suggestions/utils/types.ts +++ /dev/null @@ -1,73 +0,0 @@ -export type Method = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT'; - -export type Request = { - method: Method; - path: string; - data?: Record; -}; - -export type RequestOptions = { - /** - * Custom timeout for the request. Note that, in normal situacions - * the given timeout will be applied. But the transporter layer may - * increase this timeout if there is need for it. - */ - timeout?: number; - - /** - * Custom headers for the request. This headers are - * going to be merged the transporter headers. - */ - headers?: Record; - - /** - * Custom query parameters for the request. This query parameters are - * going to be merged the transporter query parameters. - */ - queryParameters: Record; - data?: Record; -}; - -export type EndRequest = { - method: Method; - url: string; - connectTimeout: number; - responseTimeout: number; - headers: Headers; - data?: string; -}; - -export type Response = { - content: string; - isTimedOut: boolean; - status: number; -}; - -export type EchoResponse = Request & { - connectTimeout: number; - headers: Record; - responseTimeout: number; - searchParams?: Record; - userAgent?: string; -}; - -export type Headers = Record; - -export type Host = { - url: string; - accept: 'read' | 'readWrite' | 'write'; - protocol: 'http' | 'https'; -}; - -export type StackFrame = { - request: EndRequest; - response: Response; - host: Host; - triesLeft: number; -}; - -export type Timeouts = { - readonly connect: number; - readonly read: number; - readonly write: number; -}; diff --git a/clients/algoliasearch-client-javascript/client-search/package.json b/clients/algoliasearch-client-javascript/client-search/package.json index b02aa761608..c83575b9d64 100644 --- a/clients/algoliasearch-client-javascript/client-search/package.json +++ b/clients/algoliasearch-client-javascript/client-search/package.json @@ -9,14 +9,16 @@ "main": "dist/api.js", "types": "dist/api.d.ts", "scripts": { - "clean": "rm -Rf node_modules/ *.js", "build": "tsc", - "test": "yarn build && node dist/client.js" + "clean": "rm -rf dist/" }, "engines": { "node": "^16.0.0", "yarn": "^3.0.0" }, + "dependencies": { + "@algolia/client-common": "5.0.0" + }, "devDependencies": { "@types/node": "16.11.11", "typescript": "4.5.4" diff --git a/clients/algoliasearch-client-javascript/client-search/src/apis.ts b/clients/algoliasearch-client-javascript/client-search/src/apis.ts index 7e57d462a6c..b9b43ae7077 100644 --- a/clients/algoliasearch-client-javascript/client-search/src/apis.ts +++ b/clients/algoliasearch-client-javascript/client-search/src/apis.ts @@ -1,8 +1,6 @@ import { SearchApi } from './searchApi'; export * from './searchApi'; -export * from '../utils/errors'; -export { EchoRequester } from '../utils/requester/EchoRequester'; -export { EchoResponse } from '../utils/types'; +export * from '@algolia/client-common'; export const APIS = [SearchApi]; diff --git a/clients/algoliasearch-client-javascript/client-search/src/searchApi.ts b/clients/algoliasearch-client-javascript/client-search/src/searchApi.ts index ce3d371c920..6fc26cd3e9e 100644 --- a/clients/algoliasearch-client-javascript/client-search/src/searchApi.ts +++ b/clients/algoliasearch-client-javascript/client-search/src/searchApi.ts @@ -1,3 +1,12 @@ +import { shuffle, Transporter } from '@algolia/client-common'; +import type { + Headers, + Requester, + Host, + Request, + RequestOptions, +} from '@algolia/client-common'; + import type { AddApiKeyResponse } from '../model/addApiKeyResponse'; import type { ApiKey } from '../model/apiKey'; import type { AssignUserIdParams } from '../model/assignUserIdParams'; @@ -53,10 +62,6 @@ import type { UpdatedAtResponse } from '../model/updatedAtResponse'; import type { UpdatedAtWithObjectIdResponse } from '../model/updatedAtWithObjectIdResponse'; import type { UpdatedRuleResponse } from '../model/updatedRuleResponse'; import type { UserId } from '../model/userId'; -import { Transporter } from '../utils/Transporter'; -import { shuffle } from '../utils/helpers'; -import type { Requester } from '../utils/requester/Requester'; -import type { Headers, Host, Request, RequestOptions } from '../utils/types'; export class SearchApi { protected authentications = { diff --git a/clients/algoliasearch-client-javascript/client-search/utils/Response.ts b/clients/algoliasearch-client-javascript/client-search/utils/Response.ts deleted file mode 100644 index bd22de7df9e..00000000000 --- a/clients/algoliasearch-client-javascript/client-search/utils/Response.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Response } from './types'; - -export function isNetworkError({ - isTimedOut, - status, -}: Omit): boolean { - return !isTimedOut && ~~status === 0; -} - -export function isRetryable({ - isTimedOut, - status, -}: Omit): boolean { - return ( - isTimedOut || - isNetworkError({ isTimedOut, status }) || - (~~(status / 100) !== 2 && ~~(status / 100) !== 4) - ); -} - -export function isSuccess({ status }: Pick): boolean { - return ~~(status / 100) === 2; -} diff --git a/clients/algoliasearch-client-javascript/client-search/utils/StatefulHost.ts b/clients/algoliasearch-client-javascript/client-search/utils/StatefulHost.ts deleted file mode 100644 index 162c4ed1c66..00000000000 --- a/clients/algoliasearch-client-javascript/client-search/utils/StatefulHost.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Host } from './types'; - -const EXPIRATION_DELAY = 2 * 60 * 1000; - -export class StatefulHost implements Host { - url: string; - accept: 'read' | 'readWrite' | 'write'; - protocol: 'http' | 'https'; - - private lastUpdate: number; - private status: 'down' | 'timedout' | 'up'; - - constructor(host: Host, status: StatefulHost['status'] = 'up') { - this.url = host.url; - this.accept = host.accept; - this.protocol = host.protocol; - - this.status = status; - this.lastUpdate = Date.now(); - } - - isUp(): boolean { - return ( - this.status === 'up' || Date.now() - this.lastUpdate > EXPIRATION_DELAY - ); - } - - isTimedout(): boolean { - return ( - this.status === 'timedout' && - Date.now() - this.lastUpdate <= EXPIRATION_DELAY - ); - } -} diff --git a/clients/algoliasearch-client-javascript/client-search/utils/Transporter.ts b/clients/algoliasearch-client-javascript/client-search/utils/Transporter.ts deleted file mode 100644 index 48b4edebfdb..00000000000 --- a/clients/algoliasearch-client-javascript/client-search/utils/Transporter.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { isRetryable, isSuccess } from './Response'; -import { StatefulHost } from './StatefulHost'; -import type { Cache } from './cache/Cache'; -import { MemoryCache } from './cache/MemoryCache'; -import { RetryError } from './errors'; -import { - deserializeFailure, - deserializeSuccess, - serializeData, - serializeHeaders, - serializeUrl, -} from './helpers'; -import { HttpRequester } from './requester/HttpRequester'; -import type { Requester } from './requester/Requester'; -import { - stackTraceWithoutCredentials, - stackFrameWithoutCredentials, -} from './stackTrace'; -import type { - Headers, - Host, - Request, - RequestOptions, - StackFrame, - Timeouts, - Response, - EndRequest, -} from './types'; - -export class Transporter { - private hosts: Host[]; - private baseHeaders: Headers; - private hostsCache: Cache; - private userAgent: string; - private timeouts: Timeouts; - private requester: Requester; - - constructor({ - hosts, - baseHeaders, - userAgent, - timeouts, - requester = new HttpRequester(), - }: { - hosts: Host[]; - baseHeaders: Headers; - userAgent: string; - timeouts: Timeouts; - requester?: Requester; - }) { - this.hosts = hosts; - this.hostsCache = new MemoryCache(); - this.baseHeaders = baseHeaders; - this.userAgent = userAgent; - this.timeouts = timeouts; - this.requester = requester; - } - - setHosts(hosts: Host[]): void { - this.hosts = hosts; - this.hostsCache.clear(); - } - - setRequester(requester: Requester): void { - this.requester = requester; - } - - async createRetryableOptions(compatibleHosts: Host[]): Promise<{ - hosts: Host[]; - getTimeout: (retryCount: number, timeout: number) => number; - }> { - const statefulHosts = await Promise.all( - compatibleHosts.map((statelessHost) => { - return this.hostsCache.get(statelessHost, () => { - return Promise.resolve(new StatefulHost(statelessHost)); - }); - }) - ); - const hostsUp = statefulHosts.filter((host) => host.isUp()); - const hostsTimeouted = statefulHosts.filter((host) => host.isTimedout()); - - /** - * Note, we put the hosts that previously timeouted on the end of the list. - */ - const hostsAvailable = [...hostsUp, ...hostsTimeouted]; - - const hosts = hostsAvailable.length > 0 ? hostsAvailable : compatibleHosts; - - return { - hosts, - getTimeout(timeoutsCount: number, baseTimeout: number): number { - /** - * Imagine that you have 4 hosts, if timeouts will increase - * on the following way: 1 (timeouted) > 4 (timeouted) > 5 (200). - * - * Note that, the very next request, we start from the previous timeout. - * - * 5 (timeouted) > 6 (timeouted) > 7 ... - * - * This strategy may need to be reviewed, but is the strategy on the our - * current v3 version. - */ - const timeoutMultiplier = - hostsTimeouted.length === 0 && timeoutsCount === 0 - ? 1 - : hostsTimeouted.length + 3 + timeoutsCount; - - return timeoutMultiplier * baseTimeout; - }, - }; - } - - async request( - request: Request, - requestOptions: RequestOptions - ): Promise { - const stackTrace: StackFrame[] = []; - - const isRead = request.method === 'GET'; - - /** - * First we prepare the payload that do not depend from hosts. - */ - const data = serializeData(request, requestOptions); - const headers = serializeHeaders(this.baseHeaders, requestOptions); - const method = request.method; - - // On `GET`, the data is proxied to query parameters. - const dataQueryParameters: Record = isRead - ? { - ...request.data, - ...requestOptions.data, - } - : {}; - - const queryParameters = { - 'x-algolia-agent': this.userAgent, - ...dataQueryParameters, - ...requestOptions.queryParameters, - }; - - let timeoutsCount = 0; - - const retry = async ( - hosts: Host[], - getTimeout: (timeoutsCount: number, timeout: number) => number - ): Promise => { - /** - * We iterate on each host, until there is no host left. - */ - const host = hosts.pop(); - if (host === undefined) { - throw new RetryError(stackTraceWithoutCredentials(stackTrace)); - } - - let responseTimeout = requestOptions.timeout; - if (responseTimeout === undefined) { - responseTimeout = isRead ? this.timeouts.read : this.timeouts.write; - } - - const payload: EndRequest = { - data, - headers, - method, - url: serializeUrl(host, request.path, queryParameters), - connectTimeout: getTimeout(timeoutsCount, this.timeouts.connect), - responseTimeout: getTimeout(timeoutsCount, responseTimeout), - }; - - /** - * The stackFrame is pushed to the stackTrace so we - * can have information about onRetry and onFailure - * decisions. - */ - const pushToStackTrace = (response: Response): StackFrame => { - const stackFrame: StackFrame = { - request: payload, - response, - host, - triesLeft: hosts.length, - }; - - stackTrace.push(stackFrame); - - return stackFrame; - }; - - const response = await this.requester.send(payload, request); - - if (isRetryable(response)) { - const stackFrame = pushToStackTrace(response); - - // If response is a timeout, we increase the number of timeouts so we can increase the timeout later. - if (response.isTimedOut) { - timeoutsCount++; - } - /** - * Failures are individually sent to the logger, allowing - * the end user to debug / store stack frames even - * when a retry error does not happen. - */ - // eslint-disable-next-line no-console -- this will be fixed with the new `Logger` - console.log( - 'Retryable failure', - stackFrameWithoutCredentials(stackFrame) - ); - - /** - * We also store the state of the host in failure cases. If the host, is - * down it will remain down for the next 2 minutes. In a timeout situation, - * this host will be added end of the list of hosts on the next request. - */ - await this.hostsCache.set( - host, - new StatefulHost(host, response.isTimedOut ? 'timedout' : 'down') - ); - return retry(hosts, getTimeout); - } - if (isSuccess(response)) { - return deserializeSuccess(response); - } - - pushToStackTrace(response); - throw deserializeFailure(response, stackTrace); - }; - - /** - * Finally, for each retryable host perform request until we got a non - * retryable response. Some notes here: - * - * 1. The reverse here is applied so we can apply a `pop` later on => more performant. - * 2. We also get from the retryable options a timeout multiplier that is tailored - * for the current context. - */ - const compatibleHosts = this.hosts.filter( - (host) => - host.accept === 'readWrite' || - (isRead ? host.accept === 'read' : host.accept === 'write') - ); - const options = await this.createRetryableOptions(compatibleHosts); - return retry([...options.hosts].reverse(), options.getTimeout); - } -} diff --git a/clients/algoliasearch-client-javascript/client-search/utils/cache/Cache.ts b/clients/algoliasearch-client-javascript/client-search/utils/cache/Cache.ts deleted file mode 100644 index 625862660c6..00000000000 --- a/clients/algoliasearch-client-javascript/client-search/utils/cache/Cache.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface Cache { - /** - * Gets the value of the given `key`. - */ - get: ( - key: Record | string, - defaultValue: () => Promise - ) => Promise; - - /** - * Sets the given value with the given `key`. - */ - set: ( - key: Record | string, - value: TValue - ) => Promise; - - /** - * Deletes the given `key`. - */ - delete: (key: Record | string) => Promise; - - /** - * Clears the cache. - */ - clear: () => Promise; -} diff --git a/clients/algoliasearch-client-javascript/client-search/utils/cache/MemoryCache.ts b/clients/algoliasearch-client-javascript/client-search/utils/cache/MemoryCache.ts deleted file mode 100644 index f7853f39bc4..00000000000 --- a/clients/algoliasearch-client-javascript/client-search/utils/cache/MemoryCache.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Cache } from './Cache'; - -export class MemoryCache implements Cache { - private cache: Record = {}; - - async get( - key: Record | string, - defaultValue: () => Promise - ): Promise { - const keyAsString = JSON.stringify(key); - - if (keyAsString in this.cache) { - return Promise.resolve(this.cache[keyAsString]); - } - - return await defaultValue(); - } - - set( - key: Record | string, - value: TValue - ): Promise { - this.cache[JSON.stringify(key)] = value; - - return Promise.resolve(value); - } - - delete(key: Record | string): Promise { - delete this.cache[JSON.stringify(key)]; - - return Promise.resolve(); - } - - clear(): Promise { - this.cache = {}; - - return Promise.resolve(); - } -} diff --git a/clients/algoliasearch-client-javascript/client-search/utils/errors.ts b/clients/algoliasearch-client-javascript/client-search/utils/errors.ts deleted file mode 100644 index a02f3004ad6..00000000000 --- a/clients/algoliasearch-client-javascript/client-search/utils/errors.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Response, StackFrame } from './types'; - -class ErrorWithStackTrace extends Error { - stackTrace: StackFrame[]; - - constructor(message: string, stackTrace: StackFrame[]) { - super(message); - // the array and object should be frozen to reflect the stackTrace at the time of the error - this.stackTrace = stackTrace; - } -} - -export class RetryError extends ErrorWithStackTrace { - constructor(stackTrace: StackFrame[]) { - super( - 'Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.', - stackTrace - ); - } -} - -export class ApiError extends ErrorWithStackTrace { - status: number; - - constructor(message: string, status: number, stackTrace: StackFrame[]) { - super(message, stackTrace); - this.status = status; - } -} - -export class DeserializationError extends Error { - response: Response; - - constructor(message: string, response: Response) { - super(message); - this.response = response; - } -} diff --git a/clients/algoliasearch-client-javascript/client-search/utils/helpers.ts b/clients/algoliasearch-client-javascript/client-search/utils/helpers.ts deleted file mode 100644 index 5d64ac88688..00000000000 --- a/clients/algoliasearch-client-javascript/client-search/utils/helpers.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { ApiError, DeserializationError } from './errors'; -import type { - Headers, - Host, - Request, - RequestOptions, - Response, - StackFrame, -} from './types'; - -export function shuffle(array: TData[]): TData[] { - const shuffledArray = array; - - for (let c = array.length - 1; c > 0; c--) { - const b = Math.floor(Math.random() * (c + 1)); - const a = array[c]; - - shuffledArray[c] = array[b]; - shuffledArray[b] = a; - } - - return shuffledArray; -} - -export function serializeUrl( - host: Host, - path: string, - queryParameters: Readonly> -): string { - const queryParametersAsString = serializeQueryParameters(queryParameters); - let url = `${host.protocol}://${host.url}/${ - path.charAt(0) === '/' ? path.substr(1) : path - }`; - - if (queryParametersAsString.length) { - url += `?${queryParametersAsString}`; - } - - return url; -} - -export function serializeQueryParameters( - parameters: Readonly> -): string { - const isObjectOrArray = (value: any): boolean => - Object.prototype.toString.call(value) === '[object Object]' || - Object.prototype.toString.call(value) === '[object Array]'; - - return Object.keys(parameters) - .map( - (key) => - `${key}=${ - isObjectOrArray(parameters[key]) - ? JSON.stringify(parameters[key]) - : parameters[key] - }` - ) - .join('&'); -} - -export function serializeData( - request: Request, - requestOptions: RequestOptions -): string | undefined { - if ( - request.method === 'GET' || - (request.data === undefined && requestOptions.data === undefined) - ) { - return undefined; - } - - const data = Array.isArray(request.data) - ? request.data - : { ...request.data, ...requestOptions.data }; - - return JSON.stringify(data); -} - -export function serializeHeaders( - baseHeaders: Headers, - requestOptions: RequestOptions -): Headers { - const headers: Headers = { - ...baseHeaders, - ...requestOptions.headers, - }; - const serializedHeaders: Headers = {}; - - Object.keys(headers).forEach((header) => { - const value = headers[header]; - serializedHeaders[header.toLowerCase()] = value; - }); - - return serializedHeaders; -} - -export function deserializeSuccess(response: Response): TObject { - try { - return JSON.parse(response.content); - } catch (e) { - throw new DeserializationError((e as Error).message, response); - } -} - -export function deserializeFailure( - { content, status }: Response, - stackFrame: StackFrame[] -): Error { - let message = content; - try { - message = JSON.parse(content).message; - } catch (e) { - // .. - } - - return new ApiError(message, status, stackFrame); -} diff --git a/clients/algoliasearch-client-javascript/client-search/utils/requester/EchoRequester.ts b/clients/algoliasearch-client-javascript/client-search/utils/requester/EchoRequester.ts deleted file mode 100644 index f773d0f6379..00000000000 --- a/clients/algoliasearch-client-javascript/client-search/utils/requester/EchoRequester.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { EndRequest, Request, Response, EchoResponse } from '../types'; - -import { Requester } from './Requester'; - -function searchParamsWithoutUA( - params: URLSearchParams -): EchoResponse['searchParams'] { - const searchParams = {}; - - for (const [k, v] of params) { - if (k === 'x-algolia-agent') { - continue; - } - - searchParams[k] = v; - } - - return Object.entries(searchParams).length === 0 ? undefined : searchParams; -} - -export class EchoRequester extends Requester { - constructor(private status = 200) { - super(); - } - - send( - { headers, url, connectTimeout, responseTimeout }: EndRequest, - { data, ...originalRequest }: Request - ): Promise { - const urlSearchParams = new URL(url).searchParams; - const userAgent = urlSearchParams.get('x-algolia-agent') || undefined; - const originalData = - data && Object.entries(data).length > 0 ? data : undefined; - - return Promise.resolve({ - content: JSON.stringify({ - ...originalRequest, - headers, - connectTimeout, - responseTimeout, - userAgent: userAgent ? encodeURI(userAgent) : undefined, - searchParams: searchParamsWithoutUA(urlSearchParams), - data: originalData, - }), - isTimedOut: false, - status: this.status, - }); - } -} diff --git a/clients/algoliasearch-client-javascript/client-search/utils/requester/HttpRequester.ts b/clients/algoliasearch-client-javascript/client-search/utils/requester/HttpRequester.ts deleted file mode 100644 index 3697d290fb7..00000000000 --- a/clients/algoliasearch-client-javascript/client-search/utils/requester/HttpRequester.ts +++ /dev/null @@ -1,94 +0,0 @@ -import http from 'http'; -import https from 'https'; - -import type { EndRequest, Response } from '../types'; - -import { Requester } from './Requester'; - -// Global agents allow us to reuse the TCP protocol with multiple clients -const agentOptions = { keepAlive: true }; -const httpAgent = new http.Agent(agentOptions); -const httpsAgent = new https.Agent(agentOptions); - -export class HttpRequester extends Requester { - send(request: EndRequest): Promise { - return new Promise((resolve) => { - let responseTimeout: NodeJS.Timeout | undefined; - // eslint-disable-next-line prefer-const -- linter thinks this is not reassigned - let connectTimeout: NodeJS.Timeout | undefined; - const url = new URL(request.url); - const path = - url.search === null ? url.pathname : `${url.pathname}${url.search}`; - const options: https.RequestOptions = { - agent: url.protocol === 'https:' ? httpsAgent : httpAgent, - hostname: url.hostname, - path, - method: request.method, - headers: request.headers, - ...(url.port !== undefined ? { port: url.port || '' } : {}), - }; - - const req = (url.protocol === 'https:' ? https : http).request( - options, - (response) => { - let contentBuffers: Buffer[] = []; - - response.on('data', (chunk) => { - contentBuffers = contentBuffers.concat(chunk); - }); - - response.on('end', () => { - clearTimeout(connectTimeout as NodeJS.Timeout); - clearTimeout(responseTimeout as NodeJS.Timeout); - - resolve({ - status: response.statusCode || 0, - content: Buffer.concat(contentBuffers).toString(), - isTimedOut: false, - }); - }); - } - ); - - const createTimeout = ( - timeout: number, - content: string - ): NodeJS.Timeout => { - return setTimeout(() => { - req.destroy(); - - resolve({ - status: 0, - content, - isTimedOut: true, - }); - }, timeout * 1000); - }; - - connectTimeout = createTimeout( - request.connectTimeout, - 'Connection timeout' - ); - - req.on('error', (error) => { - clearTimeout(connectTimeout as NodeJS.Timeout); - clearTimeout(responseTimeout!); - resolve({ status: 0, content: error.message, isTimedOut: false }); - }); - - req.once('response', () => { - clearTimeout(connectTimeout as NodeJS.Timeout); - responseTimeout = createTimeout( - request.responseTimeout, - 'Socket timeout' - ); - }); - - if (request.data !== undefined) { - req.write(request.data); - } - - req.end(); - }); - } -} diff --git a/clients/algoliasearch-client-javascript/client-search/utils/requester/Requester.ts b/clients/algoliasearch-client-javascript/client-search/utils/requester/Requester.ts deleted file mode 100644 index 41c06065753..00000000000 --- a/clients/algoliasearch-client-javascript/client-search/utils/requester/Requester.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { EndRequest, Request, Response } from '../types'; - -export abstract class Requester { - abstract send( - request: EndRequest, - originalRequest: Request - ): Promise; -} diff --git a/clients/algoliasearch-client-javascript/client-search/utils/stackTrace.ts b/clients/algoliasearch-client-javascript/client-search/utils/stackTrace.ts deleted file mode 100644 index 14750a54f22..00000000000 --- a/clients/algoliasearch-client-javascript/client-search/utils/stackTrace.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { StackFrame } from './types'; - -export function stackTraceWithoutCredentials( - stackTrace: StackFrame[] -): StackFrame[] { - return stackTrace.map((stackFrame) => - stackFrameWithoutCredentials(stackFrame) - ); -} - -export function stackFrameWithoutCredentials( - stackFrame: StackFrame -): StackFrame { - const modifiedHeaders: Record = stackFrame.request.headers[ - 'x-algolia-api-key' - ] - ? { 'x-algolia-api-key': '*****' } - : {}; - - return { - ...stackFrame, - request: { - ...stackFrame.request, - headers: { - ...stackFrame.request.headers, - ...modifiedHeaders, - }, - }, - }; -} diff --git a/clients/algoliasearch-client-javascript/client-search/utils/types.ts b/clients/algoliasearch-client-javascript/client-search/utils/types.ts deleted file mode 100644 index 20136fe367b..00000000000 --- a/clients/algoliasearch-client-javascript/client-search/utils/types.ts +++ /dev/null @@ -1,73 +0,0 @@ -export type Method = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT'; - -export type Request = { - method: Method; - path: string; - data?: Record; -}; - -export type RequestOptions = { - /** - * Custom timeout for the request. Note that, in normal situacions - * the given timeout will be applied. But the transporter layer may - * increase this timeout if there is need for it. - */ - timeout?: number; - - /** - * Custom headers for the request. This headers are - * going to be merged the transporter headers. - */ - headers?: Record; - - /** - * Custom query parameters for the request. This query parameters are - * going to be merged the transporter query parameters. - */ - queryParameters: Record; - data?: Record; -}; - -export type EndRequest = { - method: Method; - url: string; - connectTimeout: number; - responseTimeout: number; - headers: Headers; - data?: string; -}; - -export type Response = { - content: string; - isTimedOut: boolean; - status: number; -}; - -export type EchoResponse = Request & { - connectTimeout: number; - headers: Record; - responseTimeout: number; - searchParams?: Record; - userAgent?: string; -}; - -export type Headers = Record; - -export type Host = { - url: string; - accept: 'read' | 'readWrite' | 'write'; - protocol: 'http' | 'https'; -}; - -export type StackFrame = { - request: EndRequest; - response: Response; - host: Host; - triesLeft: number; -}; - -export type Timeouts = { - readonly connect: number; - readonly read: number; - readonly write: number; -}; diff --git a/clients/algoliasearch-client-javascript/recommend/package.json b/clients/algoliasearch-client-javascript/recommend/package.json index 9f714225d59..4389a1b4412 100644 --- a/clients/algoliasearch-client-javascript/recommend/package.json +++ b/clients/algoliasearch-client-javascript/recommend/package.json @@ -9,14 +9,16 @@ "main": "dist/api.js", "types": "dist/api.d.ts", "scripts": { - "clean": "rm -Rf node_modules/ *.js", "build": "tsc", - "test": "yarn build && node dist/client.js" + "clean": "rm -rf dist/" }, "engines": { "node": "^16.0.0", "yarn": "^3.0.0" }, + "dependencies": { + "@algolia/client-common": "5.0.0" + }, "devDependencies": { "@types/node": "16.11.11", "typescript": "4.5.4" diff --git a/clients/algoliasearch-client-javascript/recommend/src/apis.ts b/clients/algoliasearch-client-javascript/recommend/src/apis.ts index 927e908e8a8..8705167102e 100644 --- a/clients/algoliasearch-client-javascript/recommend/src/apis.ts +++ b/clients/algoliasearch-client-javascript/recommend/src/apis.ts @@ -1,8 +1,6 @@ import { RecommendApi } from './recommendApi'; export * from './recommendApi'; -export * from '../utils/errors'; -export { EchoRequester } from '../utils/requester/EchoRequester'; -export { EchoResponse } from '../utils/types'; +export * from '@algolia/client-common'; export const APIS = [RecommendApi]; diff --git a/clients/algoliasearch-client-javascript/recommend/src/recommendApi.ts b/clients/algoliasearch-client-javascript/recommend/src/recommendApi.ts index c711728eb98..538d738cf82 100644 --- a/clients/algoliasearch-client-javascript/recommend/src/recommendApi.ts +++ b/clients/algoliasearch-client-javascript/recommend/src/recommendApi.ts @@ -1,9 +1,14 @@ +import { shuffle, Transporter } from '@algolia/client-common'; +import type { + Headers, + Requester, + Host, + Request, + RequestOptions, +} from '@algolia/client-common'; + import type { GetRecommendations } from '../model/getRecommendations'; import type { GetRecommendationsResponse } from '../model/getRecommendationsResponse'; -import { Transporter } from '../utils/Transporter'; -import { shuffle } from '../utils/helpers'; -import type { Requester } from '../utils/requester/Requester'; -import type { Headers, Host, Request, RequestOptions } from '../utils/types'; export class RecommendApi { protected authentications = { diff --git a/clients/algoliasearch-client-javascript/recommend/utils/Response.ts b/clients/algoliasearch-client-javascript/recommend/utils/Response.ts deleted file mode 100644 index bd22de7df9e..00000000000 --- a/clients/algoliasearch-client-javascript/recommend/utils/Response.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Response } from './types'; - -export function isNetworkError({ - isTimedOut, - status, -}: Omit): boolean { - return !isTimedOut && ~~status === 0; -} - -export function isRetryable({ - isTimedOut, - status, -}: Omit): boolean { - return ( - isTimedOut || - isNetworkError({ isTimedOut, status }) || - (~~(status / 100) !== 2 && ~~(status / 100) !== 4) - ); -} - -export function isSuccess({ status }: Pick): boolean { - return ~~(status / 100) === 2; -} diff --git a/clients/algoliasearch-client-javascript/recommend/utils/StatefulHost.ts b/clients/algoliasearch-client-javascript/recommend/utils/StatefulHost.ts deleted file mode 100644 index 162c4ed1c66..00000000000 --- a/clients/algoliasearch-client-javascript/recommend/utils/StatefulHost.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Host } from './types'; - -const EXPIRATION_DELAY = 2 * 60 * 1000; - -export class StatefulHost implements Host { - url: string; - accept: 'read' | 'readWrite' | 'write'; - protocol: 'http' | 'https'; - - private lastUpdate: number; - private status: 'down' | 'timedout' | 'up'; - - constructor(host: Host, status: StatefulHost['status'] = 'up') { - this.url = host.url; - this.accept = host.accept; - this.protocol = host.protocol; - - this.status = status; - this.lastUpdate = Date.now(); - } - - isUp(): boolean { - return ( - this.status === 'up' || Date.now() - this.lastUpdate > EXPIRATION_DELAY - ); - } - - isTimedout(): boolean { - return ( - this.status === 'timedout' && - Date.now() - this.lastUpdate <= EXPIRATION_DELAY - ); - } -} diff --git a/clients/algoliasearch-client-javascript/recommend/utils/Transporter.ts b/clients/algoliasearch-client-javascript/recommend/utils/Transporter.ts deleted file mode 100644 index 48b4edebfdb..00000000000 --- a/clients/algoliasearch-client-javascript/recommend/utils/Transporter.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { isRetryable, isSuccess } from './Response'; -import { StatefulHost } from './StatefulHost'; -import type { Cache } from './cache/Cache'; -import { MemoryCache } from './cache/MemoryCache'; -import { RetryError } from './errors'; -import { - deserializeFailure, - deserializeSuccess, - serializeData, - serializeHeaders, - serializeUrl, -} from './helpers'; -import { HttpRequester } from './requester/HttpRequester'; -import type { Requester } from './requester/Requester'; -import { - stackTraceWithoutCredentials, - stackFrameWithoutCredentials, -} from './stackTrace'; -import type { - Headers, - Host, - Request, - RequestOptions, - StackFrame, - Timeouts, - Response, - EndRequest, -} from './types'; - -export class Transporter { - private hosts: Host[]; - private baseHeaders: Headers; - private hostsCache: Cache; - private userAgent: string; - private timeouts: Timeouts; - private requester: Requester; - - constructor({ - hosts, - baseHeaders, - userAgent, - timeouts, - requester = new HttpRequester(), - }: { - hosts: Host[]; - baseHeaders: Headers; - userAgent: string; - timeouts: Timeouts; - requester?: Requester; - }) { - this.hosts = hosts; - this.hostsCache = new MemoryCache(); - this.baseHeaders = baseHeaders; - this.userAgent = userAgent; - this.timeouts = timeouts; - this.requester = requester; - } - - setHosts(hosts: Host[]): void { - this.hosts = hosts; - this.hostsCache.clear(); - } - - setRequester(requester: Requester): void { - this.requester = requester; - } - - async createRetryableOptions(compatibleHosts: Host[]): Promise<{ - hosts: Host[]; - getTimeout: (retryCount: number, timeout: number) => number; - }> { - const statefulHosts = await Promise.all( - compatibleHosts.map((statelessHost) => { - return this.hostsCache.get(statelessHost, () => { - return Promise.resolve(new StatefulHost(statelessHost)); - }); - }) - ); - const hostsUp = statefulHosts.filter((host) => host.isUp()); - const hostsTimeouted = statefulHosts.filter((host) => host.isTimedout()); - - /** - * Note, we put the hosts that previously timeouted on the end of the list. - */ - const hostsAvailable = [...hostsUp, ...hostsTimeouted]; - - const hosts = hostsAvailable.length > 0 ? hostsAvailable : compatibleHosts; - - return { - hosts, - getTimeout(timeoutsCount: number, baseTimeout: number): number { - /** - * Imagine that you have 4 hosts, if timeouts will increase - * on the following way: 1 (timeouted) > 4 (timeouted) > 5 (200). - * - * Note that, the very next request, we start from the previous timeout. - * - * 5 (timeouted) > 6 (timeouted) > 7 ... - * - * This strategy may need to be reviewed, but is the strategy on the our - * current v3 version. - */ - const timeoutMultiplier = - hostsTimeouted.length === 0 && timeoutsCount === 0 - ? 1 - : hostsTimeouted.length + 3 + timeoutsCount; - - return timeoutMultiplier * baseTimeout; - }, - }; - } - - async request( - request: Request, - requestOptions: RequestOptions - ): Promise { - const stackTrace: StackFrame[] = []; - - const isRead = request.method === 'GET'; - - /** - * First we prepare the payload that do not depend from hosts. - */ - const data = serializeData(request, requestOptions); - const headers = serializeHeaders(this.baseHeaders, requestOptions); - const method = request.method; - - // On `GET`, the data is proxied to query parameters. - const dataQueryParameters: Record = isRead - ? { - ...request.data, - ...requestOptions.data, - } - : {}; - - const queryParameters = { - 'x-algolia-agent': this.userAgent, - ...dataQueryParameters, - ...requestOptions.queryParameters, - }; - - let timeoutsCount = 0; - - const retry = async ( - hosts: Host[], - getTimeout: (timeoutsCount: number, timeout: number) => number - ): Promise => { - /** - * We iterate on each host, until there is no host left. - */ - const host = hosts.pop(); - if (host === undefined) { - throw new RetryError(stackTraceWithoutCredentials(stackTrace)); - } - - let responseTimeout = requestOptions.timeout; - if (responseTimeout === undefined) { - responseTimeout = isRead ? this.timeouts.read : this.timeouts.write; - } - - const payload: EndRequest = { - data, - headers, - method, - url: serializeUrl(host, request.path, queryParameters), - connectTimeout: getTimeout(timeoutsCount, this.timeouts.connect), - responseTimeout: getTimeout(timeoutsCount, responseTimeout), - }; - - /** - * The stackFrame is pushed to the stackTrace so we - * can have information about onRetry and onFailure - * decisions. - */ - const pushToStackTrace = (response: Response): StackFrame => { - const stackFrame: StackFrame = { - request: payload, - response, - host, - triesLeft: hosts.length, - }; - - stackTrace.push(stackFrame); - - return stackFrame; - }; - - const response = await this.requester.send(payload, request); - - if (isRetryable(response)) { - const stackFrame = pushToStackTrace(response); - - // If response is a timeout, we increase the number of timeouts so we can increase the timeout later. - if (response.isTimedOut) { - timeoutsCount++; - } - /** - * Failures are individually sent to the logger, allowing - * the end user to debug / store stack frames even - * when a retry error does not happen. - */ - // eslint-disable-next-line no-console -- this will be fixed with the new `Logger` - console.log( - 'Retryable failure', - stackFrameWithoutCredentials(stackFrame) - ); - - /** - * We also store the state of the host in failure cases. If the host, is - * down it will remain down for the next 2 minutes. In a timeout situation, - * this host will be added end of the list of hosts on the next request. - */ - await this.hostsCache.set( - host, - new StatefulHost(host, response.isTimedOut ? 'timedout' : 'down') - ); - return retry(hosts, getTimeout); - } - if (isSuccess(response)) { - return deserializeSuccess(response); - } - - pushToStackTrace(response); - throw deserializeFailure(response, stackTrace); - }; - - /** - * Finally, for each retryable host perform request until we got a non - * retryable response. Some notes here: - * - * 1. The reverse here is applied so we can apply a `pop` later on => more performant. - * 2. We also get from the retryable options a timeout multiplier that is tailored - * for the current context. - */ - const compatibleHosts = this.hosts.filter( - (host) => - host.accept === 'readWrite' || - (isRead ? host.accept === 'read' : host.accept === 'write') - ); - const options = await this.createRetryableOptions(compatibleHosts); - return retry([...options.hosts].reverse(), options.getTimeout); - } -} diff --git a/clients/algoliasearch-client-javascript/recommend/utils/cache/Cache.ts b/clients/algoliasearch-client-javascript/recommend/utils/cache/Cache.ts deleted file mode 100644 index 625862660c6..00000000000 --- a/clients/algoliasearch-client-javascript/recommend/utils/cache/Cache.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface Cache { - /** - * Gets the value of the given `key`. - */ - get: ( - key: Record | string, - defaultValue: () => Promise - ) => Promise; - - /** - * Sets the given value with the given `key`. - */ - set: ( - key: Record | string, - value: TValue - ) => Promise; - - /** - * Deletes the given `key`. - */ - delete: (key: Record | string) => Promise; - - /** - * Clears the cache. - */ - clear: () => Promise; -} diff --git a/clients/algoliasearch-client-javascript/recommend/utils/cache/MemoryCache.ts b/clients/algoliasearch-client-javascript/recommend/utils/cache/MemoryCache.ts deleted file mode 100644 index f7853f39bc4..00000000000 --- a/clients/algoliasearch-client-javascript/recommend/utils/cache/MemoryCache.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Cache } from './Cache'; - -export class MemoryCache implements Cache { - private cache: Record = {}; - - async get( - key: Record | string, - defaultValue: () => Promise - ): Promise { - const keyAsString = JSON.stringify(key); - - if (keyAsString in this.cache) { - return Promise.resolve(this.cache[keyAsString]); - } - - return await defaultValue(); - } - - set( - key: Record | string, - value: TValue - ): Promise { - this.cache[JSON.stringify(key)] = value; - - return Promise.resolve(value); - } - - delete(key: Record | string): Promise { - delete this.cache[JSON.stringify(key)]; - - return Promise.resolve(); - } - - clear(): Promise { - this.cache = {}; - - return Promise.resolve(); - } -} diff --git a/clients/algoliasearch-client-javascript/recommend/utils/errors.ts b/clients/algoliasearch-client-javascript/recommend/utils/errors.ts deleted file mode 100644 index a02f3004ad6..00000000000 --- a/clients/algoliasearch-client-javascript/recommend/utils/errors.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Response, StackFrame } from './types'; - -class ErrorWithStackTrace extends Error { - stackTrace: StackFrame[]; - - constructor(message: string, stackTrace: StackFrame[]) { - super(message); - // the array and object should be frozen to reflect the stackTrace at the time of the error - this.stackTrace = stackTrace; - } -} - -export class RetryError extends ErrorWithStackTrace { - constructor(stackTrace: StackFrame[]) { - super( - 'Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.', - stackTrace - ); - } -} - -export class ApiError extends ErrorWithStackTrace { - status: number; - - constructor(message: string, status: number, stackTrace: StackFrame[]) { - super(message, stackTrace); - this.status = status; - } -} - -export class DeserializationError extends Error { - response: Response; - - constructor(message: string, response: Response) { - super(message); - this.response = response; - } -} diff --git a/clients/algoliasearch-client-javascript/recommend/utils/helpers.ts b/clients/algoliasearch-client-javascript/recommend/utils/helpers.ts deleted file mode 100644 index 5d64ac88688..00000000000 --- a/clients/algoliasearch-client-javascript/recommend/utils/helpers.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { ApiError, DeserializationError } from './errors'; -import type { - Headers, - Host, - Request, - RequestOptions, - Response, - StackFrame, -} from './types'; - -export function shuffle(array: TData[]): TData[] { - const shuffledArray = array; - - for (let c = array.length - 1; c > 0; c--) { - const b = Math.floor(Math.random() * (c + 1)); - const a = array[c]; - - shuffledArray[c] = array[b]; - shuffledArray[b] = a; - } - - return shuffledArray; -} - -export function serializeUrl( - host: Host, - path: string, - queryParameters: Readonly> -): string { - const queryParametersAsString = serializeQueryParameters(queryParameters); - let url = `${host.protocol}://${host.url}/${ - path.charAt(0) === '/' ? path.substr(1) : path - }`; - - if (queryParametersAsString.length) { - url += `?${queryParametersAsString}`; - } - - return url; -} - -export function serializeQueryParameters( - parameters: Readonly> -): string { - const isObjectOrArray = (value: any): boolean => - Object.prototype.toString.call(value) === '[object Object]' || - Object.prototype.toString.call(value) === '[object Array]'; - - return Object.keys(parameters) - .map( - (key) => - `${key}=${ - isObjectOrArray(parameters[key]) - ? JSON.stringify(parameters[key]) - : parameters[key] - }` - ) - .join('&'); -} - -export function serializeData( - request: Request, - requestOptions: RequestOptions -): string | undefined { - if ( - request.method === 'GET' || - (request.data === undefined && requestOptions.data === undefined) - ) { - return undefined; - } - - const data = Array.isArray(request.data) - ? request.data - : { ...request.data, ...requestOptions.data }; - - return JSON.stringify(data); -} - -export function serializeHeaders( - baseHeaders: Headers, - requestOptions: RequestOptions -): Headers { - const headers: Headers = { - ...baseHeaders, - ...requestOptions.headers, - }; - const serializedHeaders: Headers = {}; - - Object.keys(headers).forEach((header) => { - const value = headers[header]; - serializedHeaders[header.toLowerCase()] = value; - }); - - return serializedHeaders; -} - -export function deserializeSuccess(response: Response): TObject { - try { - return JSON.parse(response.content); - } catch (e) { - throw new DeserializationError((e as Error).message, response); - } -} - -export function deserializeFailure( - { content, status }: Response, - stackFrame: StackFrame[] -): Error { - let message = content; - try { - message = JSON.parse(content).message; - } catch (e) { - // .. - } - - return new ApiError(message, status, stackFrame); -} diff --git a/clients/algoliasearch-client-javascript/recommend/utils/requester/EchoRequester.ts b/clients/algoliasearch-client-javascript/recommend/utils/requester/EchoRequester.ts deleted file mode 100644 index f773d0f6379..00000000000 --- a/clients/algoliasearch-client-javascript/recommend/utils/requester/EchoRequester.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { EndRequest, Request, Response, EchoResponse } from '../types'; - -import { Requester } from './Requester'; - -function searchParamsWithoutUA( - params: URLSearchParams -): EchoResponse['searchParams'] { - const searchParams = {}; - - for (const [k, v] of params) { - if (k === 'x-algolia-agent') { - continue; - } - - searchParams[k] = v; - } - - return Object.entries(searchParams).length === 0 ? undefined : searchParams; -} - -export class EchoRequester extends Requester { - constructor(private status = 200) { - super(); - } - - send( - { headers, url, connectTimeout, responseTimeout }: EndRequest, - { data, ...originalRequest }: Request - ): Promise { - const urlSearchParams = new URL(url).searchParams; - const userAgent = urlSearchParams.get('x-algolia-agent') || undefined; - const originalData = - data && Object.entries(data).length > 0 ? data : undefined; - - return Promise.resolve({ - content: JSON.stringify({ - ...originalRequest, - headers, - connectTimeout, - responseTimeout, - userAgent: userAgent ? encodeURI(userAgent) : undefined, - searchParams: searchParamsWithoutUA(urlSearchParams), - data: originalData, - }), - isTimedOut: false, - status: this.status, - }); - } -} diff --git a/clients/algoliasearch-client-javascript/recommend/utils/requester/HttpRequester.ts b/clients/algoliasearch-client-javascript/recommend/utils/requester/HttpRequester.ts deleted file mode 100644 index 3697d290fb7..00000000000 --- a/clients/algoliasearch-client-javascript/recommend/utils/requester/HttpRequester.ts +++ /dev/null @@ -1,94 +0,0 @@ -import http from 'http'; -import https from 'https'; - -import type { EndRequest, Response } from '../types'; - -import { Requester } from './Requester'; - -// Global agents allow us to reuse the TCP protocol with multiple clients -const agentOptions = { keepAlive: true }; -const httpAgent = new http.Agent(agentOptions); -const httpsAgent = new https.Agent(agentOptions); - -export class HttpRequester extends Requester { - send(request: EndRequest): Promise { - return new Promise((resolve) => { - let responseTimeout: NodeJS.Timeout | undefined; - // eslint-disable-next-line prefer-const -- linter thinks this is not reassigned - let connectTimeout: NodeJS.Timeout | undefined; - const url = new URL(request.url); - const path = - url.search === null ? url.pathname : `${url.pathname}${url.search}`; - const options: https.RequestOptions = { - agent: url.protocol === 'https:' ? httpsAgent : httpAgent, - hostname: url.hostname, - path, - method: request.method, - headers: request.headers, - ...(url.port !== undefined ? { port: url.port || '' } : {}), - }; - - const req = (url.protocol === 'https:' ? https : http).request( - options, - (response) => { - let contentBuffers: Buffer[] = []; - - response.on('data', (chunk) => { - contentBuffers = contentBuffers.concat(chunk); - }); - - response.on('end', () => { - clearTimeout(connectTimeout as NodeJS.Timeout); - clearTimeout(responseTimeout as NodeJS.Timeout); - - resolve({ - status: response.statusCode || 0, - content: Buffer.concat(contentBuffers).toString(), - isTimedOut: false, - }); - }); - } - ); - - const createTimeout = ( - timeout: number, - content: string - ): NodeJS.Timeout => { - return setTimeout(() => { - req.destroy(); - - resolve({ - status: 0, - content, - isTimedOut: true, - }); - }, timeout * 1000); - }; - - connectTimeout = createTimeout( - request.connectTimeout, - 'Connection timeout' - ); - - req.on('error', (error) => { - clearTimeout(connectTimeout as NodeJS.Timeout); - clearTimeout(responseTimeout!); - resolve({ status: 0, content: error.message, isTimedOut: false }); - }); - - req.once('response', () => { - clearTimeout(connectTimeout as NodeJS.Timeout); - responseTimeout = createTimeout( - request.responseTimeout, - 'Socket timeout' - ); - }); - - if (request.data !== undefined) { - req.write(request.data); - } - - req.end(); - }); - } -} diff --git a/clients/algoliasearch-client-javascript/recommend/utils/requester/Requester.ts b/clients/algoliasearch-client-javascript/recommend/utils/requester/Requester.ts deleted file mode 100644 index 41c06065753..00000000000 --- a/clients/algoliasearch-client-javascript/recommend/utils/requester/Requester.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { EndRequest, Request, Response } from '../types'; - -export abstract class Requester { - abstract send( - request: EndRequest, - originalRequest: Request - ): Promise; -} diff --git a/clients/algoliasearch-client-javascript/recommend/utils/stackTrace.ts b/clients/algoliasearch-client-javascript/recommend/utils/stackTrace.ts deleted file mode 100644 index 14750a54f22..00000000000 --- a/clients/algoliasearch-client-javascript/recommend/utils/stackTrace.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { StackFrame } from './types'; - -export function stackTraceWithoutCredentials( - stackTrace: StackFrame[] -): StackFrame[] { - return stackTrace.map((stackFrame) => - stackFrameWithoutCredentials(stackFrame) - ); -} - -export function stackFrameWithoutCredentials( - stackFrame: StackFrame -): StackFrame { - const modifiedHeaders: Record = stackFrame.request.headers[ - 'x-algolia-api-key' - ] - ? { 'x-algolia-api-key': '*****' } - : {}; - - return { - ...stackFrame, - request: { - ...stackFrame.request, - headers: { - ...stackFrame.request.headers, - ...modifiedHeaders, - }, - }, - }; -} diff --git a/clients/algoliasearch-client-javascript/recommend/utils/types.ts b/clients/algoliasearch-client-javascript/recommend/utils/types.ts deleted file mode 100644 index 20136fe367b..00000000000 --- a/clients/algoliasearch-client-javascript/recommend/utils/types.ts +++ /dev/null @@ -1,73 +0,0 @@ -export type Method = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT'; - -export type Request = { - method: Method; - path: string; - data?: Record; -}; - -export type RequestOptions = { - /** - * Custom timeout for the request. Note that, in normal situacions - * the given timeout will be applied. But the transporter layer may - * increase this timeout if there is need for it. - */ - timeout?: number; - - /** - * Custom headers for the request. This headers are - * going to be merged the transporter headers. - */ - headers?: Record; - - /** - * Custom query parameters for the request. This query parameters are - * going to be merged the transporter query parameters. - */ - queryParameters: Record; - data?: Record; -}; - -export type EndRequest = { - method: Method; - url: string; - connectTimeout: number; - responseTimeout: number; - headers: Headers; - data?: string; -}; - -export type Response = { - content: string; - isTimedOut: boolean; - status: number; -}; - -export type EchoResponse = Request & { - connectTimeout: number; - headers: Record; - responseTimeout: number; - searchParams?: Record; - userAgent?: string; -}; - -export type Headers = Record; - -export type Host = { - url: string; - accept: 'read' | 'readWrite' | 'write'; - protocol: 'http' | 'https'; -}; - -export type StackFrame = { - request: EndRequest; - response: Response; - host: Host; - triesLeft: number; -}; - -export type Timeouts = { - readonly connect: number; - readonly read: number; - readonly write: number; -}; diff --git a/clients/algoliasearch-client-javascript/utils/Response.ts b/clients/algoliasearch-client-javascript/utils/Response.ts deleted file mode 100644 index bd22de7df9e..00000000000 --- a/clients/algoliasearch-client-javascript/utils/Response.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Response } from './types'; - -export function isNetworkError({ - isTimedOut, - status, -}: Omit): boolean { - return !isTimedOut && ~~status === 0; -} - -export function isRetryable({ - isTimedOut, - status, -}: Omit): boolean { - return ( - isTimedOut || - isNetworkError({ isTimedOut, status }) || - (~~(status / 100) !== 2 && ~~(status / 100) !== 4) - ); -} - -export function isSuccess({ status }: Pick): boolean { - return ~~(status / 100) === 2; -} diff --git a/clients/algoliasearch-client-javascript/utils/StatefulHost.ts b/clients/algoliasearch-client-javascript/utils/StatefulHost.ts deleted file mode 100644 index 162c4ed1c66..00000000000 --- a/clients/algoliasearch-client-javascript/utils/StatefulHost.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Host } from './types'; - -const EXPIRATION_DELAY = 2 * 60 * 1000; - -export class StatefulHost implements Host { - url: string; - accept: 'read' | 'readWrite' | 'write'; - protocol: 'http' | 'https'; - - private lastUpdate: number; - private status: 'down' | 'timedout' | 'up'; - - constructor(host: Host, status: StatefulHost['status'] = 'up') { - this.url = host.url; - this.accept = host.accept; - this.protocol = host.protocol; - - this.status = status; - this.lastUpdate = Date.now(); - } - - isUp(): boolean { - return ( - this.status === 'up' || Date.now() - this.lastUpdate > EXPIRATION_DELAY - ); - } - - isTimedout(): boolean { - return ( - this.status === 'timedout' && - Date.now() - this.lastUpdate <= EXPIRATION_DELAY - ); - } -} diff --git a/clients/algoliasearch-client-javascript/utils/Transporter.ts b/clients/algoliasearch-client-javascript/utils/Transporter.ts deleted file mode 100644 index 48b4edebfdb..00000000000 --- a/clients/algoliasearch-client-javascript/utils/Transporter.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { isRetryable, isSuccess } from './Response'; -import { StatefulHost } from './StatefulHost'; -import type { Cache } from './cache/Cache'; -import { MemoryCache } from './cache/MemoryCache'; -import { RetryError } from './errors'; -import { - deserializeFailure, - deserializeSuccess, - serializeData, - serializeHeaders, - serializeUrl, -} from './helpers'; -import { HttpRequester } from './requester/HttpRequester'; -import type { Requester } from './requester/Requester'; -import { - stackTraceWithoutCredentials, - stackFrameWithoutCredentials, -} from './stackTrace'; -import type { - Headers, - Host, - Request, - RequestOptions, - StackFrame, - Timeouts, - Response, - EndRequest, -} from './types'; - -export class Transporter { - private hosts: Host[]; - private baseHeaders: Headers; - private hostsCache: Cache; - private userAgent: string; - private timeouts: Timeouts; - private requester: Requester; - - constructor({ - hosts, - baseHeaders, - userAgent, - timeouts, - requester = new HttpRequester(), - }: { - hosts: Host[]; - baseHeaders: Headers; - userAgent: string; - timeouts: Timeouts; - requester?: Requester; - }) { - this.hosts = hosts; - this.hostsCache = new MemoryCache(); - this.baseHeaders = baseHeaders; - this.userAgent = userAgent; - this.timeouts = timeouts; - this.requester = requester; - } - - setHosts(hosts: Host[]): void { - this.hosts = hosts; - this.hostsCache.clear(); - } - - setRequester(requester: Requester): void { - this.requester = requester; - } - - async createRetryableOptions(compatibleHosts: Host[]): Promise<{ - hosts: Host[]; - getTimeout: (retryCount: number, timeout: number) => number; - }> { - const statefulHosts = await Promise.all( - compatibleHosts.map((statelessHost) => { - return this.hostsCache.get(statelessHost, () => { - return Promise.resolve(new StatefulHost(statelessHost)); - }); - }) - ); - const hostsUp = statefulHosts.filter((host) => host.isUp()); - const hostsTimeouted = statefulHosts.filter((host) => host.isTimedout()); - - /** - * Note, we put the hosts that previously timeouted on the end of the list. - */ - const hostsAvailable = [...hostsUp, ...hostsTimeouted]; - - const hosts = hostsAvailable.length > 0 ? hostsAvailable : compatibleHosts; - - return { - hosts, - getTimeout(timeoutsCount: number, baseTimeout: number): number { - /** - * Imagine that you have 4 hosts, if timeouts will increase - * on the following way: 1 (timeouted) > 4 (timeouted) > 5 (200). - * - * Note that, the very next request, we start from the previous timeout. - * - * 5 (timeouted) > 6 (timeouted) > 7 ... - * - * This strategy may need to be reviewed, but is the strategy on the our - * current v3 version. - */ - const timeoutMultiplier = - hostsTimeouted.length === 0 && timeoutsCount === 0 - ? 1 - : hostsTimeouted.length + 3 + timeoutsCount; - - return timeoutMultiplier * baseTimeout; - }, - }; - } - - async request( - request: Request, - requestOptions: RequestOptions - ): Promise { - const stackTrace: StackFrame[] = []; - - const isRead = request.method === 'GET'; - - /** - * First we prepare the payload that do not depend from hosts. - */ - const data = serializeData(request, requestOptions); - const headers = serializeHeaders(this.baseHeaders, requestOptions); - const method = request.method; - - // On `GET`, the data is proxied to query parameters. - const dataQueryParameters: Record = isRead - ? { - ...request.data, - ...requestOptions.data, - } - : {}; - - const queryParameters = { - 'x-algolia-agent': this.userAgent, - ...dataQueryParameters, - ...requestOptions.queryParameters, - }; - - let timeoutsCount = 0; - - const retry = async ( - hosts: Host[], - getTimeout: (timeoutsCount: number, timeout: number) => number - ): Promise => { - /** - * We iterate on each host, until there is no host left. - */ - const host = hosts.pop(); - if (host === undefined) { - throw new RetryError(stackTraceWithoutCredentials(stackTrace)); - } - - let responseTimeout = requestOptions.timeout; - if (responseTimeout === undefined) { - responseTimeout = isRead ? this.timeouts.read : this.timeouts.write; - } - - const payload: EndRequest = { - data, - headers, - method, - url: serializeUrl(host, request.path, queryParameters), - connectTimeout: getTimeout(timeoutsCount, this.timeouts.connect), - responseTimeout: getTimeout(timeoutsCount, responseTimeout), - }; - - /** - * The stackFrame is pushed to the stackTrace so we - * can have information about onRetry and onFailure - * decisions. - */ - const pushToStackTrace = (response: Response): StackFrame => { - const stackFrame: StackFrame = { - request: payload, - response, - host, - triesLeft: hosts.length, - }; - - stackTrace.push(stackFrame); - - return stackFrame; - }; - - const response = await this.requester.send(payload, request); - - if (isRetryable(response)) { - const stackFrame = pushToStackTrace(response); - - // If response is a timeout, we increase the number of timeouts so we can increase the timeout later. - if (response.isTimedOut) { - timeoutsCount++; - } - /** - * Failures are individually sent to the logger, allowing - * the end user to debug / store stack frames even - * when a retry error does not happen. - */ - // eslint-disable-next-line no-console -- this will be fixed with the new `Logger` - console.log( - 'Retryable failure', - stackFrameWithoutCredentials(stackFrame) - ); - - /** - * We also store the state of the host in failure cases. If the host, is - * down it will remain down for the next 2 minutes. In a timeout situation, - * this host will be added end of the list of hosts on the next request. - */ - await this.hostsCache.set( - host, - new StatefulHost(host, response.isTimedOut ? 'timedout' : 'down') - ); - return retry(hosts, getTimeout); - } - if (isSuccess(response)) { - return deserializeSuccess(response); - } - - pushToStackTrace(response); - throw deserializeFailure(response, stackTrace); - }; - - /** - * Finally, for each retryable host perform request until we got a non - * retryable response. Some notes here: - * - * 1. The reverse here is applied so we can apply a `pop` later on => more performant. - * 2. We also get from the retryable options a timeout multiplier that is tailored - * for the current context. - */ - const compatibleHosts = this.hosts.filter( - (host) => - host.accept === 'readWrite' || - (isRead ? host.accept === 'read' : host.accept === 'write') - ); - const options = await this.createRetryableOptions(compatibleHosts); - return retry([...options.hosts].reverse(), options.getTimeout); - } -} diff --git a/clients/algoliasearch-client-javascript/utils/cache/Cache.ts b/clients/algoliasearch-client-javascript/utils/cache/Cache.ts deleted file mode 100644 index 625862660c6..00000000000 --- a/clients/algoliasearch-client-javascript/utils/cache/Cache.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface Cache { - /** - * Gets the value of the given `key`. - */ - get: ( - key: Record | string, - defaultValue: () => Promise - ) => Promise; - - /** - * Sets the given value with the given `key`. - */ - set: ( - key: Record | string, - value: TValue - ) => Promise; - - /** - * Deletes the given `key`. - */ - delete: (key: Record | string) => Promise; - - /** - * Clears the cache. - */ - clear: () => Promise; -} diff --git a/clients/algoliasearch-client-javascript/utils/cache/MemoryCache.ts b/clients/algoliasearch-client-javascript/utils/cache/MemoryCache.ts deleted file mode 100644 index f7853f39bc4..00000000000 --- a/clients/algoliasearch-client-javascript/utils/cache/MemoryCache.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Cache } from './Cache'; - -export class MemoryCache implements Cache { - private cache: Record = {}; - - async get( - key: Record | string, - defaultValue: () => Promise - ): Promise { - const keyAsString = JSON.stringify(key); - - if (keyAsString in this.cache) { - return Promise.resolve(this.cache[keyAsString]); - } - - return await defaultValue(); - } - - set( - key: Record | string, - value: TValue - ): Promise { - this.cache[JSON.stringify(key)] = value; - - return Promise.resolve(value); - } - - delete(key: Record | string): Promise { - delete this.cache[JSON.stringify(key)]; - - return Promise.resolve(); - } - - clear(): Promise { - this.cache = {}; - - return Promise.resolve(); - } -} diff --git a/clients/algoliasearch-client-javascript/utils/errors.ts b/clients/algoliasearch-client-javascript/utils/errors.ts deleted file mode 100644 index a02f3004ad6..00000000000 --- a/clients/algoliasearch-client-javascript/utils/errors.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Response, StackFrame } from './types'; - -class ErrorWithStackTrace extends Error { - stackTrace: StackFrame[]; - - constructor(message: string, stackTrace: StackFrame[]) { - super(message); - // the array and object should be frozen to reflect the stackTrace at the time of the error - this.stackTrace = stackTrace; - } -} - -export class RetryError extends ErrorWithStackTrace { - constructor(stackTrace: StackFrame[]) { - super( - 'Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.', - stackTrace - ); - } -} - -export class ApiError extends ErrorWithStackTrace { - status: number; - - constructor(message: string, status: number, stackTrace: StackFrame[]) { - super(message, stackTrace); - this.status = status; - } -} - -export class DeserializationError extends Error { - response: Response; - - constructor(message: string, response: Response) { - super(message); - this.response = response; - } -} diff --git a/clients/algoliasearch-client-javascript/utils/helpers.ts b/clients/algoliasearch-client-javascript/utils/helpers.ts deleted file mode 100644 index 5d64ac88688..00000000000 --- a/clients/algoliasearch-client-javascript/utils/helpers.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { ApiError, DeserializationError } from './errors'; -import type { - Headers, - Host, - Request, - RequestOptions, - Response, - StackFrame, -} from './types'; - -export function shuffle(array: TData[]): TData[] { - const shuffledArray = array; - - for (let c = array.length - 1; c > 0; c--) { - const b = Math.floor(Math.random() * (c + 1)); - const a = array[c]; - - shuffledArray[c] = array[b]; - shuffledArray[b] = a; - } - - return shuffledArray; -} - -export function serializeUrl( - host: Host, - path: string, - queryParameters: Readonly> -): string { - const queryParametersAsString = serializeQueryParameters(queryParameters); - let url = `${host.protocol}://${host.url}/${ - path.charAt(0) === '/' ? path.substr(1) : path - }`; - - if (queryParametersAsString.length) { - url += `?${queryParametersAsString}`; - } - - return url; -} - -export function serializeQueryParameters( - parameters: Readonly> -): string { - const isObjectOrArray = (value: any): boolean => - Object.prototype.toString.call(value) === '[object Object]' || - Object.prototype.toString.call(value) === '[object Array]'; - - return Object.keys(parameters) - .map( - (key) => - `${key}=${ - isObjectOrArray(parameters[key]) - ? JSON.stringify(parameters[key]) - : parameters[key] - }` - ) - .join('&'); -} - -export function serializeData( - request: Request, - requestOptions: RequestOptions -): string | undefined { - if ( - request.method === 'GET' || - (request.data === undefined && requestOptions.data === undefined) - ) { - return undefined; - } - - const data = Array.isArray(request.data) - ? request.data - : { ...request.data, ...requestOptions.data }; - - return JSON.stringify(data); -} - -export function serializeHeaders( - baseHeaders: Headers, - requestOptions: RequestOptions -): Headers { - const headers: Headers = { - ...baseHeaders, - ...requestOptions.headers, - }; - const serializedHeaders: Headers = {}; - - Object.keys(headers).forEach((header) => { - const value = headers[header]; - serializedHeaders[header.toLowerCase()] = value; - }); - - return serializedHeaders; -} - -export function deserializeSuccess(response: Response): TObject { - try { - return JSON.parse(response.content); - } catch (e) { - throw new DeserializationError((e as Error).message, response); - } -} - -export function deserializeFailure( - { content, status }: Response, - stackFrame: StackFrame[] -): Error { - let message = content; - try { - message = JSON.parse(content).message; - } catch (e) { - // .. - } - - return new ApiError(message, status, stackFrame); -} diff --git a/clients/algoliasearch-client-javascript/utils/requester/EchoRequester.ts b/clients/algoliasearch-client-javascript/utils/requester/EchoRequester.ts deleted file mode 100644 index f773d0f6379..00000000000 --- a/clients/algoliasearch-client-javascript/utils/requester/EchoRequester.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { EndRequest, Request, Response, EchoResponse } from '../types'; - -import { Requester } from './Requester'; - -function searchParamsWithoutUA( - params: URLSearchParams -): EchoResponse['searchParams'] { - const searchParams = {}; - - for (const [k, v] of params) { - if (k === 'x-algolia-agent') { - continue; - } - - searchParams[k] = v; - } - - return Object.entries(searchParams).length === 0 ? undefined : searchParams; -} - -export class EchoRequester extends Requester { - constructor(private status = 200) { - super(); - } - - send( - { headers, url, connectTimeout, responseTimeout }: EndRequest, - { data, ...originalRequest }: Request - ): Promise { - const urlSearchParams = new URL(url).searchParams; - const userAgent = urlSearchParams.get('x-algolia-agent') || undefined; - const originalData = - data && Object.entries(data).length > 0 ? data : undefined; - - return Promise.resolve({ - content: JSON.stringify({ - ...originalRequest, - headers, - connectTimeout, - responseTimeout, - userAgent: userAgent ? encodeURI(userAgent) : undefined, - searchParams: searchParamsWithoutUA(urlSearchParams), - data: originalData, - }), - isTimedOut: false, - status: this.status, - }); - } -} diff --git a/clients/algoliasearch-client-javascript/utils/requester/HttpRequester.ts b/clients/algoliasearch-client-javascript/utils/requester/HttpRequester.ts deleted file mode 100644 index 3697d290fb7..00000000000 --- a/clients/algoliasearch-client-javascript/utils/requester/HttpRequester.ts +++ /dev/null @@ -1,94 +0,0 @@ -import http from 'http'; -import https from 'https'; - -import type { EndRequest, Response } from '../types'; - -import { Requester } from './Requester'; - -// Global agents allow us to reuse the TCP protocol with multiple clients -const agentOptions = { keepAlive: true }; -const httpAgent = new http.Agent(agentOptions); -const httpsAgent = new https.Agent(agentOptions); - -export class HttpRequester extends Requester { - send(request: EndRequest): Promise { - return new Promise((resolve) => { - let responseTimeout: NodeJS.Timeout | undefined; - // eslint-disable-next-line prefer-const -- linter thinks this is not reassigned - let connectTimeout: NodeJS.Timeout | undefined; - const url = new URL(request.url); - const path = - url.search === null ? url.pathname : `${url.pathname}${url.search}`; - const options: https.RequestOptions = { - agent: url.protocol === 'https:' ? httpsAgent : httpAgent, - hostname: url.hostname, - path, - method: request.method, - headers: request.headers, - ...(url.port !== undefined ? { port: url.port || '' } : {}), - }; - - const req = (url.protocol === 'https:' ? https : http).request( - options, - (response) => { - let contentBuffers: Buffer[] = []; - - response.on('data', (chunk) => { - contentBuffers = contentBuffers.concat(chunk); - }); - - response.on('end', () => { - clearTimeout(connectTimeout as NodeJS.Timeout); - clearTimeout(responseTimeout as NodeJS.Timeout); - - resolve({ - status: response.statusCode || 0, - content: Buffer.concat(contentBuffers).toString(), - isTimedOut: false, - }); - }); - } - ); - - const createTimeout = ( - timeout: number, - content: string - ): NodeJS.Timeout => { - return setTimeout(() => { - req.destroy(); - - resolve({ - status: 0, - content, - isTimedOut: true, - }); - }, timeout * 1000); - }; - - connectTimeout = createTimeout( - request.connectTimeout, - 'Connection timeout' - ); - - req.on('error', (error) => { - clearTimeout(connectTimeout as NodeJS.Timeout); - clearTimeout(responseTimeout!); - resolve({ status: 0, content: error.message, isTimedOut: false }); - }); - - req.once('response', () => { - clearTimeout(connectTimeout as NodeJS.Timeout); - responseTimeout = createTimeout( - request.responseTimeout, - 'Socket timeout' - ); - }); - - if (request.data !== undefined) { - req.write(request.data); - } - - req.end(); - }); - } -} diff --git a/clients/algoliasearch-client-javascript/utils/requester/Requester.ts b/clients/algoliasearch-client-javascript/utils/requester/Requester.ts deleted file mode 100644 index 41c06065753..00000000000 --- a/clients/algoliasearch-client-javascript/utils/requester/Requester.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { EndRequest, Request, Response } from '../types'; - -export abstract class Requester { - abstract send( - request: EndRequest, - originalRequest: Request - ): Promise; -} diff --git a/clients/algoliasearch-client-javascript/utils/stackTrace.ts b/clients/algoliasearch-client-javascript/utils/stackTrace.ts deleted file mode 100644 index 14750a54f22..00000000000 --- a/clients/algoliasearch-client-javascript/utils/stackTrace.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { StackFrame } from './types'; - -export function stackTraceWithoutCredentials( - stackTrace: StackFrame[] -): StackFrame[] { - return stackTrace.map((stackFrame) => - stackFrameWithoutCredentials(stackFrame) - ); -} - -export function stackFrameWithoutCredentials( - stackFrame: StackFrame -): StackFrame { - const modifiedHeaders: Record = stackFrame.request.headers[ - 'x-algolia-api-key' - ] - ? { 'x-algolia-api-key': '*****' } - : {}; - - return { - ...stackFrame, - request: { - ...stackFrame.request, - headers: { - ...stackFrame.request.headers, - ...modifiedHeaders, - }, - }, - }; -} diff --git a/clients/algoliasearch-client-javascript/utils/types.ts b/clients/algoliasearch-client-javascript/utils/types.ts deleted file mode 100644 index 20136fe367b..00000000000 --- a/clients/algoliasearch-client-javascript/utils/types.ts +++ /dev/null @@ -1,73 +0,0 @@ -export type Method = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT'; - -export type Request = { - method: Method; - path: string; - data?: Record; -}; - -export type RequestOptions = { - /** - * Custom timeout for the request. Note that, in normal situacions - * the given timeout will be applied. But the transporter layer may - * increase this timeout if there is need for it. - */ - timeout?: number; - - /** - * Custom headers for the request. This headers are - * going to be merged the transporter headers. - */ - headers?: Record; - - /** - * Custom query parameters for the request. This query parameters are - * going to be merged the transporter query parameters. - */ - queryParameters: Record; - data?: Record; -}; - -export type EndRequest = { - method: Method; - url: string; - connectTimeout: number; - responseTimeout: number; - headers: Headers; - data?: string; -}; - -export type Response = { - content: string; - isTimedOut: boolean; - status: number; -}; - -export type EchoResponse = Request & { - connectTimeout: number; - headers: Record; - responseTimeout: number; - searchParams?: Record; - userAgent?: string; -}; - -export type Headers = Record; - -export type Host = { - url: string; - accept: 'read' | 'readWrite' | 'write'; - protocol: 'http' | 'https'; -}; - -export type StackFrame = { - request: EndRequest; - response: Response; - host: Host; - triesLeft: number; -}; - -export type Timeouts = { - readonly connect: number; - readonly read: number; - readonly write: number; -}; diff --git a/doc/contribution_addNewClient.md b/doc/contribution_addNewClient.md index 409670c186e..5d70cc79210 100644 --- a/doc/contribution_addNewClient.md +++ b/doc/contribution_addNewClient.md @@ -53,7 +53,7 @@ See this PR of the first JavaScript implementation for reference: https://github ### Retry strategy -The retry strategy cannot be generated and needs to be implemented outside of the generated client folder. You need to add your transporter to the `utils/` folder, and update the `.mustache` template files accordingly. +The retry strategy cannot be generated and needs to be implemented outside of the generated client folder. You need to add your transporter to the utils folder of your language, and update the `.mustache` template files accordingly. See this PR of the first JavaScript implementation for reference: https://github.com/algolia/api-client-automation-experiment/pull/9 diff --git a/package.json b/package.json index 87bc173ef87..e3c882ec4c7 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "docker": "docker exec -it dev yarn $*", "lint": "eslint --ext=ts .", "post:generate": "./scripts/post-gen/global.sh", - "generate": "./scripts/multiplexer.sh ${2:-nonverbose} ./scripts/generate.sh ${0:-all} ${1:-all} && yarn post:generate", + "generate": "./scripts/multiplexer.sh ${2:-nonverbose} ./scripts/generate.sh ${0:-all} ${1:-all} && yarn post:generate ${0:-all}", "playground": "./scripts/multiplexer.sh ${2:-nonverbose} ./scripts/playground.sh ${0:-javascript} ${1:-search}", "specs:format": "yarn prettier --write specs", "specs:lint": "eslint --ext=yml specs/ .github/ && yarn openapi lint" diff --git a/scripts/post-gen/global.sh b/scripts/post-gen/global.sh index 90f28574f47..e0663554a17 100755 --- a/scripts/post-gen/global.sh +++ b/scripts/post-gen/global.sh @@ -1,5 +1,7 @@ #!/bin/bash +LANGUAGE=$1 + if [[ $CI ]]; then exit 0 fi @@ -10,8 +12,17 @@ if [[ ! $DOCKER ]]; then exit 1 fi +build_js_common() { + echo "> Building @algolia/client-common..." + + yarn workspace @algolia/client-common build + + echo "" +} + format_specs() { echo "> Formatting specs..." + CMD="yarn specs:format" if [[ $VERBOSE == "true" ]]; then $CMD @@ -25,6 +36,12 @@ format_specs() { fi set -e fi + + echo "" } format_specs + +if [[ $LANGUAGE == 'javascript' || $LANGUAGE == 'all' ]]; then + build_js_common +fi diff --git a/scripts/post-gen/javascript.sh b/scripts/post-gen/javascript.sh index 1f27a99b6f2..c75a676ba97 100755 --- a/scripts/post-gen/javascript.sh +++ b/scripts/post-gen/javascript.sh @@ -3,11 +3,6 @@ export GENERATOR=$1 export CLIENT=$(cat openapitools.json | jq -r --arg generator "$GENERATOR" '."generator-cli".generators[$generator].output' | sed 's/#{cwd}\///g') -echo "> Exporting utils for ${GENERATOR}..." -mkdir -p $CLIENT/utils - -cp -R clients/algoliasearch-client-javascript/utils/ $CLIENT/ - lint_client() { echo "> Linting ${GENERATOR}..." CMD="yarn eslint --ext=ts ${CLIENT} --fix" diff --git a/templates/javascript/api-all.mustache b/templates/javascript/api-all.mustache index cb29f7ade87..17954c9dfc9 100644 --- a/templates/javascript/api-all.mustache +++ b/templates/javascript/api-all.mustache @@ -8,9 +8,7 @@ import { {{ classname }} } from './{{ classFilename }}'; export * from './{{ classFilename }}Interface' {{/withInterfaces}} {{/apis}} -export * from '../utils/errors'; -export { EchoRequester } from '../utils/requester/EchoRequester'; -export { EchoResponse } from '../utils/types'; +export * from '@algolia/client-common'; export const APIS = [{{#apis}}{{#operations}}{{ classname }}{{/operations}}{{^-last}}, {{/-last}}{{/apis}}]; {{/apiInfo}} diff --git a/templates/javascript/api-single.mustache b/templates/javascript/api-single.mustache index 6fb1cd3ea31..c3dd04237df 100644 --- a/templates/javascript/api-single.mustache +++ b/templates/javascript/api-single.mustache @@ -1,7 +1,5 @@ -import { shuffle } from '../utils/helpers'; -import { Transporter } from '../utils/Transporter'; -import { Headers, Host, Request, RequestOptions } from '../utils/types'; -import { Requester } from '../utils/requester/Requester'; +import { shuffle, Transporter } from '@algolia/client-common'; +import type { Headers, Requester, Host, Request, RequestOptions } from '@algolia/client-common'; {{#imports}} import { {{classname}} } from '{{filename}}'; diff --git a/templates/javascript/package.mustache b/templates/javascript/package.mustache index 19d95599004..a61bf5d26ec 100644 --- a/templates/javascript/package.mustache +++ b/templates/javascript/package.mustache @@ -9,14 +9,16 @@ "main": "dist/api.js", "types": "dist/api.d.ts", "scripts": { - "clean": "rm -Rf node_modules/ *.js", "build": "tsc", - "test": "yarn build && node dist/client.js" + "clean": "rm -rf dist/" }, "engines": { "node": "^16.0.0", "yarn": "^3.0.0" }, + "dependencies": { + "@algolia/client-common": "5.0.0" + }, "devDependencies": { "@types/node": "16.11.11", "typescript": "4.5.4" diff --git a/tests/package.json b/tests/package.json index cdc30638071..bcc6b8dfe77 100644 --- a/tests/package.json +++ b/tests/package.json @@ -6,9 +6,10 @@ ], "scripts": { "build": "tsc", - "generate": "yarn generate:methods:requets ${0:-javascript} ${1:-search}", "generate:methods:requets": "node dist/tests/src/methods/requests/main.js ${0:-javascript} ${1:-search}", - "start": "yarn build && yarn generate ${0:-javascript} ${1:-search} && yarn lint:fix" + "generate": "yarn generate:methods:requets ${0:-javascript} ${1:-search} && yarn lint:fix", + "lint:fix": "yarn workspace javascript-tests lint:fix", + "start": "yarn build && yarn generate ${0:-javascript} ${1:-search}" }, "devDependencies": { "@apidevtools/swagger-parser": "10.0.3", diff --git a/tests/src/methods/requests/cts.ts b/tests/src/methods/requests/cts.ts index 1ab4af46389..7cb95924242 100644 --- a/tests/src/methods/requests/cts.ts +++ b/tests/src/methods/requests/cts.ts @@ -3,9 +3,10 @@ import fsp from 'fs/promises'; import SwaggerParser from '@apidevtools/swagger-parser'; import type { OpenAPIV3 } from 'openapi-types'; -import type { CTS, CTSBlock, Tests } from './types'; import { removeObjectName, walk } from '../../utils'; +import type { CTS, CTSBlock, Tests } from './types'; + async function loadRequestsCTS(client: string): Promise { // load the list of operations from the spec const spec = await SwaggerParser.validate(`../specs/${client}/spec.yml`); diff --git a/tests/src/methods/requests/generate.ts b/tests/src/methods/requests/generate.ts index 244d2f17a83..617d032e7ea 100644 --- a/tests/src/methods/requests/generate.ts +++ b/tests/src/methods/requests/generate.ts @@ -2,15 +2,16 @@ import fsp from 'fs/promises'; import Mustache from 'mustache'; -import { loadCTS } from './cts'; -import { loadRequestsTemplate } from './templates'; -import type { CTSBlock } from './types'; import { createClientName, packageNames, extensionForLanguage, } from '../../utils'; +import { loadCTS } from './cts'; +import { loadRequestsTemplate } from './templates'; +import type { CTSBlock } from './types'; + async function createOutputDir(language: string): Promise { await fsp.mkdir(`output/${language}/tests/methods/requests`, { recursive: true, diff --git a/tests/src/methods/requests/main.ts b/tests/src/methods/requests/main.ts index b8b5ba95bf5..8973444b5c0 100644 --- a/tests/src/methods/requests/main.ts +++ b/tests/src/methods/requests/main.ts @@ -1,8 +1,9 @@ /* eslint-disable no-console */ -import { generateTests } from './generate'; import { packageNames } from '../../utils'; +import { generateTests } from './generate'; + function printUsage(): void { console.log(`usage: generateCTS language client`); // eslint-disable-next-line no-process-exit diff --git a/yarn.lock b/yarn.lock index 3a4b9ec3273..c69a302824c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36,6 +36,7 @@ __metadata: version: 0.0.0-use.local resolution: "@algolia/client-abtesting@workspace:clients/algoliasearch-client-javascript/client-abtesting" dependencies: + "@algolia/client-common": 5.0.0 "@types/node": 16.11.11 typescript: 4.5.4 languageName: unknown @@ -44,6 +45,16 @@ __metadata: "@algolia/client-analytics@5.0.0, @algolia/client-analytics@workspace:clients/algoliasearch-client-javascript/client-analytics": version: 0.0.0-use.local resolution: "@algolia/client-analytics@workspace:clients/algoliasearch-client-javascript/client-analytics" + dependencies: + "@algolia/client-common": 5.0.0 + "@types/node": 16.11.11 + typescript: 4.5.4 + languageName: unknown + linkType: soft + +"@algolia/client-common@5.0.0, @algolia/client-common@workspace:clients/algoliasearch-client-javascript/client-common": + version: 0.0.0-use.local + resolution: "@algolia/client-common@workspace:clients/algoliasearch-client-javascript/client-common" dependencies: "@types/node": 16.11.11 typescript: 4.5.4 @@ -54,6 +65,7 @@ __metadata: version: 0.0.0-use.local resolution: "@algolia/client-insights@workspace:clients/algoliasearch-client-javascript/client-insights" dependencies: + "@algolia/client-common": 5.0.0 "@types/node": 16.11.11 typescript: 4.5.4 languageName: unknown @@ -63,6 +75,7 @@ __metadata: version: 0.0.0-use.local resolution: "@algolia/client-personalization@workspace:clients/algoliasearch-client-javascript/client-personalization" dependencies: + "@algolia/client-common": 5.0.0 "@types/node": 16.11.11 typescript: 4.5.4 languageName: unknown @@ -72,6 +85,7 @@ __metadata: version: 0.0.0-use.local resolution: "@algolia/client-query-suggestions@workspace:clients/algoliasearch-client-javascript/client-query-suggestions" dependencies: + "@algolia/client-common": 5.0.0 "@types/node": 16.11.11 typescript: 4.5.4 languageName: unknown @@ -81,6 +95,7 @@ __metadata: version: 0.0.0-use.local resolution: "@algolia/client-search@workspace:clients/algoliasearch-client-javascript/client-search" dependencies: + "@algolia/client-common": 5.0.0 "@types/node": 16.11.11 typescript: 4.5.4 languageName: unknown @@ -90,6 +105,7 @@ __metadata: version: 0.0.0-use.local resolution: "@algolia/recommend@workspace:clients/algoliasearch-client-javascript/recommend" dependencies: + "@algolia/client-common": 5.0.0 "@types/node": 16.11.11 typescript: 4.5.4 languageName: unknown