diff --git a/etc/astra-db-ts.api.md b/etc/astra-db-ts.api.md index c617d219..9cb9ccbc 100644 --- a/etc/astra-db-ts.api.md +++ b/etc/astra-db-ts.api.md @@ -94,10 +94,10 @@ export interface AddVectorizeOperation { // @public export abstract class AdminCommandEvent extends BaseClientEvent { - // Warning: (ae-forgotten-export) The symbol "DevOpsAPIRequestInfo" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "DevOpsAPIRequestMetadata" needs to be exported by the entry point index.d.ts // // @internal - protected constructor(name: string, requestId: string, baseUrl: string, info: DevOpsAPIRequestInfo, longRunning: boolean); + protected constructor(name: string, metadata: DevOpsAPIRequestMetadata); // (undocumented) getMessagePrefix(): string; readonly invokingMethod: string; @@ -122,7 +122,7 @@ export type AdminCommandEventMap = { // @public export class AdminCommandFailedEvent extends AdminCommandEvent { // @internal - constructor(requestId: string, baseUrl: string, info: DevOpsAPIRequestInfo, longRunning: boolean, error: Error, started: number); + constructor(metadata: DevOpsAPIRequestMetadata, error: Error); readonly duration: number; readonly error: Error; // (undocumented) @@ -134,7 +134,7 @@ export class AdminCommandFailedEvent extends AdminCommandEvent { // @public export class AdminCommandPollingEvent extends AdminCommandEvent { // @internal - constructor(requestId: string, baseUrl: string, info: DevOpsAPIRequestInfo, started: number, interval: number, pollCount: number); + constructor(metadata: DevOpsAPIRequestMetadata, interval: number, pollCount: number); readonly elapsed: number; // (undocumented) getMessage(): string; @@ -147,7 +147,7 @@ export class AdminCommandPollingEvent extends AdminCommandEvent { // @public export class AdminCommandStartedEvent extends AdminCommandEvent { // @internal - constructor(requestId: string, baseUrl: string, info: DevOpsAPIRequestInfo, longRunning: boolean, timeout: Partial); + constructor(metadata: DevOpsAPIRequestMetadata); // (undocumented) getMessage(): string; // @internal @@ -158,7 +158,7 @@ export class AdminCommandStartedEvent extends AdminCommandEvent { // @public export class AdminCommandSucceededEvent extends AdminCommandEvent { // @internal - constructor(requestId: string, baseUrl: string, info: DevOpsAPIRequestInfo, longRunning: boolean, data: Record | undefined, started: number); + constructor(metadata: DevOpsAPIRequestMetadata, data: Record | undefined); readonly duration: number; // (undocumented) getMessage(): string; @@ -170,7 +170,7 @@ export class AdminCommandSucceededEvent extends AdminCommandEvent { // @public export class AdminCommandWarningsEvent extends AdminCommandEvent { // @internal - constructor(requestId: string, baseUrl: string, info: DevOpsAPIRequestInfo, longRunning: boolean, warnings: NonEmpty); + constructor(metadata: DevOpsAPIRequestMetadata, warnings: NonEmpty); // (undocumented) getMessage(): string; // @internal @@ -386,8 +386,10 @@ export class AWSEmbeddingHeadersProvider extends StaticHeadersProvider<'embeddin // @public export abstract class BaseClientEvent { + // Warning: (ae-forgotten-export) The symbol "RequestId" needs to be exported by the entry point index.d.ts + // // @internal - protected constructor(name: string, requestId: string, extra: Record | undefined); + protected constructor(name: string, requestId: RequestId, extra: Record | undefined); readonly extraLogInfo?: Record; format(formatter?: EventFormatter): string; formatVerbose(): string; @@ -855,10 +857,10 @@ export type CollTypeCodecOpts = TypeCodecOpts | undefined); + protected constructor(name: string, metadata: DataAPIRequestMetadata); readonly command: Record; get commandName(): string; // @internal (undocumented) @@ -904,7 +906,7 @@ export type CommandEventTarget = { // @public export class CommandFailedEvent extends CommandEvent { // @internal - constructor(requestId: string, info: DataAPIRequestInfo, extra: Record | undefined, reply: RawDataAPIResponse | undefined, error: Error, started: number); + constructor(metadata: DataAPIRequestMetadata, reply: RawDataAPIResponse | undefined, error: Error); readonly duration: number; readonly error: Error; // (undocumented) @@ -918,8 +920,14 @@ export class CommandFailedEvent extends CommandEvent { // @public export interface CommandOptions> { + // (undocumented) + isSafelyRetryable?: boolean; // @deprecated maxTimeMS?: 'ERROR: The `maxTimeMS` option is no longer available; the timeouts system has been overhauled, and timeouts should now be set using `timeout`'; + // Warning: (ae-forgotten-export) The symbol "RetryConfig" needs to be exported by the entry point index.d.ts + // + // (undocumented) + retry?: RetryConfig; timeout?: number | Pick, 'requestTimeoutMs' | Exclude>; } @@ -932,7 +940,7 @@ export interface CommandOptionsSpec { // @public export class CommandStartedEvent extends CommandEvent { // @internal - constructor(requestId: string, info: DataAPIRequestInfo, extra: Record | undefined); + constructor(metadata: DataAPIRequestMetadata); // (undocumented) getMessage(): string; // @internal @@ -943,7 +951,7 @@ export class CommandStartedEvent extends CommandEvent { // @public export class CommandSucceededEvent extends CommandEvent { // @internal - constructor(requestId: string, info: DataAPIRequestInfo, extra: Record | undefined, reply: RawDataAPIResponse, started: number); + constructor(metadata: DataAPIRequestMetadata, reply: RawDataAPIResponse); readonly duration: number; // (undocumented) getMessage(): string; @@ -955,7 +963,7 @@ export class CommandSucceededEvent extends CommandEvent { // @public export class CommandWarningsEvent extends CommandEvent { // @internal - constructor(requestId: string, info: DataAPIRequestInfo, extra: Record | undefined, warnings: NonEmpty); + constructor(metadata: DataAPIRequestMetadata, warnings: NonEmpty); // (undocumented) getMessage(): string; // @internal @@ -1327,8 +1335,6 @@ export class DataAPITimeoutError extends DataAPIError { // // @internal constructor(info: HTTPRequestInfo, types: TimedOutCategories); - // @internal (undocumented) - static mk(this: void, info: HTTPRequestInfo, types: TimedOutCategories): DataAPITimeoutError; readonly timedOutCategories: TimedOutCategories; readonly timeout: Partial; } @@ -1523,8 +1529,6 @@ export class DevOpsAPIResponseError extends DevOpsAPIError { export class DevOpsAPITimeoutError extends DevOpsAPIError { // @internal constructor(info: HTTPRequestInfo, types: TimedOutCategories); - // @internal (undocumented) - static mk(this: void, info: HTTPRequestInfo, types: TimedOutCategories): DevOpsAPITimeoutError; readonly timedOutCategories: TimedOutCategories; readonly timeout: Partial; readonly url: string; @@ -2228,6 +2232,11 @@ export type MaybeId = NoId & { _id?: IdOf; }; +// @internal (undocumented) +export type Mut = { + -readonly [K in keyof T]: T[K]; +}; + // @public export type NoId = Omit; diff --git a/examples/astra-db-ts.tgz b/examples/astra-db-ts.tgz index b08d0c58..053c5849 100644 Binary files a/examples/astra-db-ts.tgz and b/examples/astra-db-ts.tgz differ diff --git a/examples/browser/package-lock.json b/examples/browser/package-lock.json index 45c99d2d..162da9e9 100644 --- a/examples/browser/package-lock.json +++ b/examples/browser/package-lock.json @@ -59,7 +59,7 @@ "node_modules/@datastax/astra-db-ts": { "version": "2.0.1", "resolved": "file:../astra-db-ts.tgz", - "integrity": "sha512-+3ilS93UuowJb0OFm4N/Y+tDrEVlA+Snyda+W/QHABg4Kg3wB6tcYdGEr4iuLK44hoPg4mpgKH5OUXD8cWPUuw==", + "integrity": "sha512-f6VXv36BvqKhdEnMSvDqCeDlC2K1tvWL7bQI/h9gJogXFfgM6FGbCCzwQ/FXeTERfXOPRs2udeP8dx+YabFc+A==", "license": "Apache-2.0", "dependencies": { "bignumber.js": "^9.1.2", diff --git a/examples/cloudflare-workers/package-lock.json b/examples/cloudflare-workers/package-lock.json index db435f31..75028eae 100644 --- a/examples/cloudflare-workers/package-lock.json +++ b/examples/cloudflare-workers/package-lock.json @@ -178,7 +178,7 @@ "node_modules/@datastax/astra-db-ts": { "version": "2.0.1", "resolved": "file:../astra-db-ts.tgz", - "integrity": "sha512-+3ilS93UuowJb0OFm4N/Y+tDrEVlA+Snyda+W/QHABg4Kg3wB6tcYdGEr4iuLK44hoPg4mpgKH5OUXD8cWPUuw==", + "integrity": "sha512-f6VXv36BvqKhdEnMSvDqCeDlC2K1tvWL7bQI/h9gJogXFfgM6FGbCCzwQ/FXeTERfXOPRs2udeP8dx+YabFc+A==", "license": "Apache-2.0", "dependencies": { "bignumber.js": "^9.1.2", diff --git a/examples/customize-http/package-lock.json b/examples/customize-http/package-lock.json index 7a1f6cb1..0f0c5bae 100644 --- a/examples/customize-http/package-lock.json +++ b/examples/customize-http/package-lock.json @@ -83,7 +83,7 @@ "node_modules/@datastax/astra-db-ts": { "version": "2.0.1", "resolved": "file:../astra-db-ts.tgz", - "integrity": "sha512-+3ilS93UuowJb0OFm4N/Y+tDrEVlA+Snyda+W/QHABg4Kg3wB6tcYdGEr4iuLK44hoPg4mpgKH5OUXD8cWPUuw==", + "integrity": "sha512-f6VXv36BvqKhdEnMSvDqCeDlC2K1tvWL7bQI/h9gJogXFfgM6FGbCCzwQ/FXeTERfXOPRs2udeP8dx+YabFc+A==", "license": "Apache-2.0", "dependencies": { "bignumber.js": "^9.1.2", diff --git a/examples/logging/package-lock.json b/examples/logging/package-lock.json index 489caf56..b3a4b10f 100644 --- a/examples/logging/package-lock.json +++ b/examples/logging/package-lock.json @@ -80,7 +80,7 @@ "node_modules/@datastax/astra-db-ts": { "version": "2.0.1", "resolved": "file:../astra-db-ts.tgz", - "integrity": "sha512-+3ilS93UuowJb0OFm4N/Y+tDrEVlA+Snyda+W/QHABg4Kg3wB6tcYdGEr4iuLK44hoPg4mpgKH5OUXD8cWPUuw==", + "integrity": "sha512-f6VXv36BvqKhdEnMSvDqCeDlC2K1tvWL7bQI/h9gJogXFfgM6FGbCCzwQ/FXeTERfXOPRs2udeP8dx+YabFc+A==", "license": "Apache-2.0", "dependencies": { "bignumber.js": "^9.1.2", diff --git a/examples/nextjs/package-lock.json b/examples/nextjs/package-lock.json index ed78014b..1e92c86a 100644 --- a/examples/nextjs/package-lock.json +++ b/examples/nextjs/package-lock.json @@ -64,7 +64,7 @@ "node_modules/@datastax/astra-db-ts": { "version": "2.0.1", "resolved": "file:../astra-db-ts.tgz", - "integrity": "sha512-+3ilS93UuowJb0OFm4N/Y+tDrEVlA+Snyda+W/QHABg4Kg3wB6tcYdGEr4iuLK44hoPg4mpgKH5OUXD8cWPUuw==", + "integrity": "sha512-f6VXv36BvqKhdEnMSvDqCeDlC2K1tvWL7bQI/h9gJogXFfgM6FGbCCzwQ/FXeTERfXOPRs2udeP8dx+YabFc+A==", "license": "Apache-2.0", "dependencies": { "bignumber.js": "^9.1.2", diff --git a/examples/non-astra-backends/package-lock.json b/examples/non-astra-backends/package-lock.json index c5652c61..25184633 100644 --- a/examples/non-astra-backends/package-lock.json +++ b/examples/non-astra-backends/package-lock.json @@ -59,7 +59,7 @@ "node_modules/@datastax/astra-db-ts": { "version": "2.0.1", "resolved": "file:../astra-db-ts.tgz", - "integrity": "sha512-+3ilS93UuowJb0OFm4N/Y+tDrEVlA+Snyda+W/QHABg4Kg3wB6tcYdGEr4iuLK44hoPg4mpgKH5OUXD8cWPUuw==", + "integrity": "sha512-f6VXv36BvqKhdEnMSvDqCeDlC2K1tvWL7bQI/h9gJogXFfgM6FGbCCzwQ/FXeTERfXOPRs2udeP8dx+YabFc+A==", "license": "Apache-2.0", "dependencies": { "bignumber.js": "^9.1.2", diff --git a/examples/serdes/package-lock.json b/examples/serdes/package-lock.json index 913632f1..5b40a251 100644 --- a/examples/serdes/package-lock.json +++ b/examples/serdes/package-lock.json @@ -61,7 +61,7 @@ "node_modules/@datastax/astra-db-ts": { "version": "2.0.1", "resolved": "file:../astra-db-ts.tgz", - "integrity": "sha512-+3ilS93UuowJb0OFm4N/Y+tDrEVlA+Snyda+W/QHABg4Kg3wB6tcYdGEr4iuLK44hoPg4mpgKH5OUXD8cWPUuw==", + "integrity": "sha512-f6VXv36BvqKhdEnMSvDqCeDlC2K1tvWL7bQI/h9gJogXFfgM6FGbCCzwQ/FXeTERfXOPRs2udeP8dx+YabFc+A==", "license": "Apache-2.0", "dependencies": { "bignumber.js": "^9.1.2", diff --git a/examples/using-http2/package-lock.json b/examples/using-http2/package-lock.json index 10d8cc6e..bbc587b4 100644 --- a/examples/using-http2/package-lock.json +++ b/examples/using-http2/package-lock.json @@ -65,7 +65,7 @@ "node_modules/@datastax/astra-db-ts": { "version": "2.0.1", "resolved": "file:../astra-db-ts.tgz", - "integrity": "sha512-+3ilS93UuowJb0OFm4N/Y+tDrEVlA+Snyda+W/QHABg4Kg3wB6tcYdGEr4iuLK44hoPg4mpgKH5OUXD8cWPUuw==", + "integrity": "sha512-f6VXv36BvqKhdEnMSvDqCeDlC2K1tvWL7bQI/h9gJogXFfgM6FGbCCzwQ/FXeTERfXOPRs2udeP8dx+YabFc+A==", "license": "Apache-2.0", "dependencies": { "bignumber.js": "^9.1.2", diff --git a/scripts/check.ts b/scripts/check.ts index 080ded69..4d3c0f21 100755 --- a/scripts/check.ts +++ b/scripts/check.ts @@ -152,6 +152,10 @@ function LibCheck(): Step { }); } catch (_) { Utils.printFailed('Library compilation failed'); + + await spinner('Rerunning tsc with captured output...', async () => { + await $({ stdio: 'inherit', nothrow: true })`npx tsc`; + }); } } else { Utils.printFailed('Could not set up library for lib-check phase'); diff --git a/src/administration/astra-admin.ts b/src/administration/astra-admin.ts index acfdcb45..75042e82 100644 --- a/src/administration/astra-admin.ts +++ b/src/administration/astra-admin.ts @@ -23,7 +23,7 @@ import { AstraDbAdmin } from '@/src/administration/astra-db-admin.js'; import { Db } from '@/src/db/db.js'; import { buildAstraDatabaseAdminInfo } from '@/src/administration/utils.js'; import { DEFAULT_DEVOPS_API_ENDPOINTS, DEFAULT_KEYSPACE, HttpMethods } from '@/src/lib/api/constants.js'; -import { DevOpsAPIHttpClient } from '@/src/lib/api/clients/devops-api-http-client.js'; +import { DevOpsAPIHttpClient } from '@/src/lib/api/clients/impls/devops-api-http-client.js'; import type { CommandOptions, OpaqueHttpClient } from '@/src/lib/index.js'; import { HierarchicalLogger, TokenProvider } from '@/src/lib/index.js'; import type { AstraFullDatabaseInfo } from '@/src/administration/types/admin/database-info.js'; @@ -283,7 +283,8 @@ export class AstraAdmin extends HierarchicalLogger { method: HttpMethods.Get, path: `/databases/${id}`, methodName: 'admin.dbInfo', - }, tm); + timeoutManager: tm, + }); return buildAstraDatabaseAdminInfo(resp.data!, this.#environment); } @@ -341,7 +342,8 @@ export class AstraAdmin extends HierarchicalLogger { path: `/databases`, params: params, methodName: 'admin.listDatabases', - }, tm); + timeoutManager: tm, + }); return resp.data!.map((d: SomeDoc) => buildAstraDatabaseAdminInfo(d, this.#environment)); } @@ -414,12 +416,12 @@ export class AstraAdmin extends HierarchicalLogger { path: '/databases', data: definition, methodName: 'admin.createDatabase', + timeoutManager: tm, }, { id: (resp) => resp.headers.location, target: 'ACTIVE', legalStates: ['INITIALIZING', 'PENDING'], defaultPollInterval: 10000, - timeoutManager: tm, options, }); @@ -462,12 +464,12 @@ export class AstraAdmin extends HierarchicalLogger { method: HttpMethods.Post, path: `/databases/${id}/terminate`, methodName: 'admin.dropDatabase', + timeoutManager: tm, }, { id: id, target: 'TERMINATED', legalStates: ['TERMINATING'], defaultPollInterval: 10000, - timeoutManager: tm, options, }); } @@ -524,7 +526,8 @@ export class AstraAdmin extends HierarchicalLogger { 'region-type': 'vector', }, methodName: 'admin.findAvailableRegions', - }, tm); + timeoutManager: tm, + }); return resp.data!.map((region: any): AstraAvailableRegionInfo => ({ classification: region.classification, diff --git a/src/administration/astra-db-admin.ts b/src/administration/astra-db-admin.ts index bbb58644..75e68641 100644 --- a/src/administration/astra-db-admin.ts +++ b/src/administration/astra-db-admin.ts @@ -19,8 +19,8 @@ import type { OpaqueHttpClient, CommandOptions } from '@/src/lib/index.js'; import { TokenProvider } from '@/src/lib/index.js'; import { buildAstraDatabaseAdminInfo, extractAstraEnvironment } from '@/src/administration/utils.js'; import { DEFAULT_DEVOPS_API_ENDPOINTS, HttpMethods } from '@/src/lib/api/constants.js'; -import type { DevOpsAPIRequestInfo } from '@/src/lib/api/clients/devops-api-http-client.js'; -import { DevOpsAPIHttpClient } from '@/src/lib/api/clients/devops-api-http-client.js'; +import type { ExecuteDevOpsAPIOperationOptions } from '@/src/lib/api/clients/impls/devops-api-http-client.js'; +import { DevOpsAPIHttpClient } from '@/src/lib/api/clients/impls/devops-api-http-client.js'; import type { Db } from '@/src/db/index.js'; import { $CustomInspect } from '@/src/lib/constants.js'; import type { AstraFullDatabaseInfo } from '@/src/administration/types/admin/database-info.js'; @@ -212,12 +212,12 @@ export class AstraDbAdmin extends DbAdmin { method: HttpMethods.Post, path: `/databases/${this.#db.id}/keyspaces/${keyspace}`, methodName: 'dbAdmin.createKeyspace', + timeoutManager: tm, }, { id: this.#db.id, target: 'ACTIVE', legalStates: ['MAINTENANCE'], defaultPollInterval: 1000, - timeoutManager: tm, options, }); } @@ -260,12 +260,12 @@ export class AstraDbAdmin extends DbAdmin { method: HttpMethods.Delete, path: `/databases/${this.#db.id}/keyspaces/${keyspace}`, methodName: 'dbAdmin.dropKeyspace', + timeoutManager: tm, }, { id: this.#db.id, target: 'ACTIVE', legalStates: ['MAINTENANCE'], defaultPollInterval: 1000, - timeoutManager: tm, options, }); } @@ -298,12 +298,12 @@ export class AstraDbAdmin extends DbAdmin { method: HttpMethods.Post, path: `/databases/${this.#db.id}/terminate`, methodName: 'dbAdmin.drop', + timeoutManager: tm, }, { id: this.#db.id, target: 'TERMINATED', legalStates: ['TERMINATING'], defaultPollInterval: 10000, - timeoutManager: tm, options, }); } @@ -312,12 +312,13 @@ export class AstraDbAdmin extends DbAdmin { return this.#httpClient; } - async #info(methodName: DevOpsAPIRequestInfo['methodName'], tm: TimeoutManager): Promise { + async #info(methodName: ExecuteDevOpsAPIOperationOptions['methodName'], tm: TimeoutManager): Promise { const resp = await this.#httpClient.request({ method: HttpMethods.Get, path: `/databases/${this.#db.id}`, + timeoutManager: tm, methodName, - }, tm); + }); return buildAstraDatabaseAdminInfo(resp.data!, this.#environment); } diff --git a/src/administration/data-api-db-admin.ts b/src/administration/data-api-db-admin.ts index bb74d804..269d53c5 100644 --- a/src/administration/data-api-db-admin.ts +++ b/src/administration/data-api-db-admin.ts @@ -16,7 +16,7 @@ import type { CreateDataAPIKeyspaceOptions } from '@/src/administration/types/index.js'; import { DbAdmin } from '@/src/administration/db-admin.js'; import type { OpaqueHttpClient, CommandOptions } from '@/src/lib/index.js'; -import type { DataAPIHttpClient } from '@/src/lib/api/clients/data-api-http-client.js'; +import type { DataAPIHttpClient } from '@/src/lib/api/clients/impls/data-api-http-client.js'; import type { Db } from '@/src/db/index.js'; import { $CustomInspect } from '@/src/lib/constants.js'; import type { ParsedAdminOptions } from '@/src/client/opts-handlers/admin-opts-handler.js'; diff --git a/src/administration/errors.ts b/src/administration/errors.ts index 9faaea43..2fe39eb8 100644 --- a/src/administration/errors.ts +++ b/src/administration/errors.ts @@ -87,13 +87,6 @@ export class DevOpsAPITimeoutError extends DevOpsAPIError { this.timedOutCategories = types; this.name = 'DevOpsAPITimeoutError'; } - - /** - * @internal - */ - public static mk(this: void, info: HTTPRequestInfo, types: TimedOutCategories): DevOpsAPITimeoutError { - return new DevOpsAPITimeoutError(info, types); - } } /** diff --git a/src/administration/events.ts b/src/administration/events.ts index f4dbd92b..a34c9fa6 100644 --- a/src/administration/events.ts +++ b/src/administration/events.ts @@ -13,7 +13,7 @@ // limitations under the License. // import { DataAPIClientEvent } from '@/src/lib/logging/events'; needs to be like this or it errors -import type { DevOpsAPIRequestInfo } from '@/src/lib/api/clients/devops-api-http-client.js'; +import type { DevOpsAPIRequestMetadata } from '@/src/lib/api/clients/impls/devops-api-http-client.js'; import type { DataAPIWarningDescriptor } from '@/src/documents/index.js'; import { BaseClientEvent } from '@/src/lib/logging/base-event.js'; import type { TimeoutDescriptor } from '@/src/lib/api/timeouts/timeouts.js'; @@ -90,14 +90,14 @@ export abstract class AdminCommandEvent extends BaseClientEvent { * * @internal */ - protected constructor(name: string, requestId: string, baseUrl: string, info: DevOpsAPIRequestInfo, longRunning: boolean) { - super(name, requestId, undefined); - this.url = baseUrl + info.path; - this.requestMethod = info.method; - this.requestBody = info.data; - this.requestParams = info.params; - this.isLongRunning = longRunning; - this.invokingMethod = info.methodName; + protected constructor(name: string, metadata: DevOpsAPIRequestMetadata) { + super(name, metadata.requestId, undefined); + this.url = metadata.baseUrl + metadata.reqOpts.path; + this.requestMethod = metadata.reqOpts.method; + this.requestBody = metadata.reqOpts.data; + this.requestParams = metadata.reqOpts.params; + this.isLongRunning = metadata.isLongRunning; + this.invokingMethod = metadata.reqOpts.methodName; } public override getMessagePrefix() { @@ -135,9 +135,9 @@ export class AdminCommandStartedEvent extends AdminCommandEvent { * * @internal */ - constructor(requestId: string, baseUrl: string, info: DevOpsAPIRequestInfo, longRunning: boolean, timeout: Partial) { - super('AdminCommandStarted', requestId, baseUrl, info, longRunning); - this.timeout = timeout; + constructor(metadata: DevOpsAPIRequestMetadata) { + super('AdminCommandStarted', metadata); + this.timeout = metadata.timeout; } public override getMessage(): string { @@ -182,9 +182,9 @@ export class AdminCommandPollingEvent extends AdminCommandEvent { * * @internal */ - constructor(requestId: string, baseUrl: string, info: DevOpsAPIRequestInfo, started: number, interval: number, pollCount: number) { - super('AdminCommandPolling', requestId, baseUrl, info, true); - this.elapsed = performance.now() - started; + constructor(metadata: DevOpsAPIRequestMetadata, interval: number, pollCount: number) { + super('AdminCommandPolling', metadata); + this.elapsed = performance.now() - metadata.startTime; this.pollInterval = interval; this.pollCount = pollCount; } @@ -224,9 +224,9 @@ export class AdminCommandSucceededEvent extends AdminCommandEvent { * * @internal */ - constructor(requestId: string, baseUrl: string, info: DevOpsAPIRequestInfo, longRunning: boolean, data: Record | undefined, started: number) { - super('AdminCommandSucceeded', requestId, baseUrl, info, longRunning); - this.duration = performance.now() - started; + constructor(metadata: DevOpsAPIRequestMetadata, data: Record | undefined) { + super('AdminCommandSucceeded', metadata); + this.duration = performance.now() - metadata.startTime; this.responseBody = data || undefined; } @@ -268,9 +268,9 @@ export class AdminCommandFailedEvent extends AdminCommandEvent { * * @internal */ - constructor(requestId: string, baseUrl: string, info: DevOpsAPIRequestInfo, longRunning: boolean, error: Error, started: number) { - super('AdminCommandFailed', requestId, baseUrl, info, longRunning); - this.duration = performance.now() - started; + constructor(metadata: DevOpsAPIRequestMetadata, error: Error) { + super('AdminCommandFailed', metadata); + this.duration = performance.now() - metadata.startTime; this.error = error; } @@ -304,8 +304,8 @@ export class AdminCommandWarningsEvent extends AdminCommandEvent { * * @internal */ - constructor(requestId: string, baseUrl: string, info: DevOpsAPIRequestInfo, longRunning: boolean, warnings: NonEmpty) { - super('AdminCommandWarnings', requestId, baseUrl, info, longRunning); + constructor(metadata: DevOpsAPIRequestMetadata, warnings: NonEmpty) { + super('AdminCommandWarnings', metadata); this.warnings = warnings; } diff --git a/src/db/db.ts b/src/db/db.ts index 1f345cae..2c1f9d83 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -22,7 +22,7 @@ import { extractDbComponentsFromAstraUrl } from '@/src/documents/utils.js'; import type { DbAdmin } from '@/src/administration/index.js'; import { DataAPIDbAdmin } from '@/src/administration/data-api-db-admin.js'; import type { CreateCollectionOptions } from '@/src/db/types/collections/create.js'; -import { DataAPIHttpClient, EmissionStrategy } from '@/src/lib/api/clients/data-api-http-client.js'; +import { DataAPIHttpClient, EmissionStrategy } from '@/src/lib/api/clients/impls/data-api-http-client.js'; import type { KeyspaceRef } from '@/src/lib/api/clients/types.js'; import type { CommandEventMap, FoundRow, SomePKey, SomeRow, TableDropIndexOptions } from '@/src/documents/index.js'; import { Table } from '@/src/documents/index.js'; @@ -177,9 +177,8 @@ export class Db extends HierarchicalLogger { : endpoint; this.#httpClient = new DataAPIHttpClient({ - baseUrl: endpoint, + baseUrl: endpoint + '/' + (this.#defaultOpts.dbOptions.dataApiPath || DEFAULT_DATA_API_PATHS[rootOpts.environment]), tokenProvider: this.#defaultOpts.dbOptions.token, - baseApiPath: this.#defaultOpts.dbOptions.dataApiPath || DEFAULT_DATA_API_PATHS[rootOpts.environment], logger: this, fetchCtx: rootOpts.fetchCtx, keyspace: this.#keyspace, diff --git a/src/documents/collections/collection.ts b/src/documents/collections/collection.ts index e6e01a03..d07ce55a 100644 --- a/src/documents/collections/collection.ts +++ b/src/documents/collections/collection.ts @@ -53,7 +53,7 @@ import type { DropCollectionOptions, WithKeyspace, } from '@/src/db/index.js'; -import type { BigNumberHack, DataAPIHttpClient } from '@/src/lib/api/clients/data-api-http-client.js'; +import type { BigNumberHack, DataAPIHttpClient } from '@/src/lib/api/clients/impls/data-api-http-client.js'; import { HierarchicalLogger } from '@/src/lib/logging/hierarchical-logger.js'; import type { OpaqueHttpClient, CommandOptions } from '@/src/lib/index.js'; import { CommandImpls } from '@/src/documents/commands/command-impls.js'; diff --git a/src/documents/errors.ts b/src/documents/errors.ts index 613c6205..cc327c52 100644 --- a/src/documents/errors.ts +++ b/src/documents/errors.ts @@ -240,13 +240,6 @@ export class DataAPITimeoutError extends DataAPIError { this.timedOutCategories = types; this.name = 'DataAPITimeoutError'; } - - /** - * @internal - */ - public static mk(this: void, info: HTTPRequestInfo, types: TimedOutCategories): DataAPITimeoutError { - return new DataAPITimeoutError(info, types); - } } /** diff --git a/src/documents/events.ts b/src/documents/events.ts index 6d3a15b4..cbd893da 100644 --- a/src/documents/events.ts +++ b/src/documents/events.ts @@ -15,7 +15,7 @@ import type { NonEmpty, ReadonlyNonEmpty } from '@/src/lib/index.js'; import { type RawDataAPIResponse } from '@/src/lib/index.js'; import { BaseClientEvent } from '@/src/lib/logging/base-event.js'; -import type { DataAPIRequestInfo } from '@/src/lib/api/clients/data-api-http-client.js'; +import type { DataAPIRequestMetadata } from '@/src/lib/api/clients/impls/data-api-http-client.js'; import type { DataAPIWarningDescriptor } from '@/src/documents/errors.js'; import { DataAPIError } from '@/src/documents/errors.js'; import type { TimeoutDescriptor } from '@/src/lib/api/timeouts/timeouts.js'; @@ -50,11 +50,6 @@ export type CommandEventMap = { commandWarnings: CommandWarningsEvent, } -// export type CommandEventTarget = { url: string } & ( -// | { keyspace: string } & ({ table: string, collection?: never } | { collection: string, table?: never }) -// | { keyspace?: never, table?: never, collection?: never } -// ) - /** * The target of the command. * @@ -66,20 +61,6 @@ export type CommandEventTarget = | { url: string, keyspace: string, table?: never, collection: string } | { url: string, keyspace: string, table: string, collection?: never } -const mkCommandEventTarget = (info: DataAPIRequestInfo): Readonly => { - const target = { url: info.url } as CommandEventTarget; - - if (info.keyspace) { - target.keyspace = info.keyspace; - } - - if (info.tOrCType) { - target[info.tOrCType] = info.tOrC; - } - - return target; -}; - /** * Common base class for all command events. * @@ -113,10 +94,10 @@ export abstract class CommandEvent extends BaseClientEvent { * * @internal */ - protected constructor(name: string, requestId: string, info: DataAPIRequestInfo, extra: Record | undefined) { - super(name, requestId, extra); - this.command = info.command; - this.target = mkCommandEventTarget(info); + protected constructor(name: string, metadata: DataAPIRequestMetadata) { + super(name, metadata.requestId, metadata.extra); + this.command = metadata.command; + this.target = metadata.target; } /** @@ -180,9 +161,9 @@ export class CommandStartedEvent extends CommandEvent { * * @internal */ - constructor(requestId: string, info: DataAPIRequestInfo, extra: Record | undefined) { - super('CommandStarted', requestId, info, extra); - this.timeout = info.timeoutManager.initial(); + constructor(metadata: DataAPIRequestMetadata) { + super('CommandStarted', metadata); + this.timeout = metadata.timeout; } public override getMessage(): string { @@ -223,9 +204,9 @@ export class CommandSucceededEvent extends CommandEvent { * * @internal */ - constructor(requestId: string, info: DataAPIRequestInfo, extra: Record | undefined, reply: RawDataAPIResponse, started: number) { - super('CommandSucceeded', requestId, info, extra); - this.duration = performance.now() - started; + constructor(metadata: DataAPIRequestMetadata, reply: RawDataAPIResponse) { + super('CommandSucceeded', metadata); + this.duration = performance.now() - metadata.startTime; this.response = reply; } @@ -274,9 +255,9 @@ export class CommandFailedEvent extends CommandEvent { * * @internal */ - constructor(requestId: string, info: DataAPIRequestInfo, extra: Record | undefined, reply: RawDataAPIResponse | undefined, error: Error, started: number) { - super('CommandFailed', requestId, info, extra); - this.duration = performance.now() - started; + constructor(metadata: DataAPIRequestMetadata, reply: RawDataAPIResponse | undefined, error: Error) { + super('CommandFailed', metadata); + this.duration = performance.now() - metadata.startTime; this.response = reply; this.error = error; } @@ -318,8 +299,8 @@ export class CommandWarningsEvent extends CommandEvent { * * @internal */ - constructor(requestId: string, info: DataAPIRequestInfo, extra: Record | undefined, warnings: NonEmpty) { - super('CommandWarnings', requestId, info, extra); + constructor(metadata: DataAPIRequestMetadata, warnings: NonEmpty) { + super('CommandWarnings', metadata); this.warnings = warnings; } diff --git a/src/documents/tables/table.ts b/src/documents/tables/table.ts index 2707a61a..e9dcf13d 100644 --- a/src/documents/tables/table.ts +++ b/src/documents/tables/table.ts @@ -34,7 +34,7 @@ import type { WithSim, } from '@/src/documents/index.js'; import { TableFindCursor, TableInsertManyError } from '@/src/documents/index.js'; -import type { BigNumberHack, DataAPIHttpClient } from '@/src/lib/api/clients/data-api-http-client.js'; +import type { BigNumberHack, DataAPIHttpClient } from '@/src/lib/api/clients/impls/data-api-http-client.js'; import { CommandImpls } from '@/src/documents/commands/command-impls.js'; import type { AlterTableOptions, diff --git a/src/index.ts b/src/index.ts index 4447521b..1c0e8c41 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,3 +19,65 @@ export * from './administration/index.js'; export * from './lib/index.js'; export * from './version.js'; export { BigNumber } from 'bignumber.js'; + +// import { RequestId } from '@/src/lib/api/clients/utils/request-id.js'; +// import { RetryManager } from '@/src/lib/api/retries/manager.js'; +// import { DataAPIRetryAdapter } from '@/src/lib/api/retries/adapters/data-api.js'; +// import { RetryPolicy } from '@/src/lib/api/retries/policy.js'; +// import { DataAPIClient } from '@/src/client/index.js'; +// import { Timeouts } from '@/src/lib/api/timeouts/timeouts.js'; +// import type { HTTPRequestInfo } from '@/src/lib/api/clients/index.js'; +// +// const rm = RetryManager.mk(true, {}, new DataAPIRetryAdapter(new DataAPIClient()), { +// defaultPolicy: new class extends RetryPolicy.Default { +// maxRetries = () => 10; +// shouldResetTimeout = () => true; +// }, +// } as any); +// +// const dummyMd = { +// startTime: 0, +// timeout: {}, +// target: null!, +// requestId: new RequestId(), +// extra: {}, +// command: {}, +// }; +// +// const dummyTm = new Timeouts({ mkTimeoutError: (info: HTTPRequestInfo) => new Error(JSON.stringify(info)) }, Timeouts.Default) +// .single('generalMethodTimeoutMs', {}); +// +// await Promise.all([ +// simulateRetrying('#1', 2), +// simulateRetrying('#2', 2), +// simulateRetrying('#3', 2), +// ]); +// +// await simulateRetrying('#4', 2); +// +// await Promise.all([ +// simulateRetrying('#5', 2), +// simulateRetrying('#6', 2), +// simulateRetrying('#7', 2), +// ]); +// +// async function simulateRetrying(id: string, retryTimes: number) { +// let i = 0; +// const startTime = Date.now(); +// +// await rm.run(dummyMd, dummyTm, async () => { +// if (i < retryTimes) { +// console.log(`[${id}] Starting retry run after ${Date.now() - startTime}ms`); +// } else { +// console.log(`[${id}] Starting successful run after ${Date.now() - startTime}ms`); +// } +// +// await new Promise(resolve => setTimeout(resolve, 500)); +// +// if (i < retryTimes) { +// throw new Error(`${i++}`); +// } +// +// return Promise.resolve(4); +// }); +// } diff --git a/src/lib/api/clients/data-api-http-client.ts b/src/lib/api/clients/data-api-http-client.ts deleted file mode 100644 index 6a0a52c4..00000000 --- a/src/lib/api/clients/data-api-http-client.ts +++ /dev/null @@ -1,306 +0,0 @@ -// Copyright DataStax, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// noinspection ExceptionCaughtLocallyJS - -import type { InternalLogger } from '@/src/lib/logging/internal-logger.js'; -import type { NonEmpty, RawDataAPIResponse } from '@/src/lib/index.js'; -import { - EmbeddingAPIKeyHeaderProvider, - HeadersProvider, - RerankingAPIKeyHeaderProvider, - TokenProvider, -} from '@/src/lib/index.js'; -import type { DataAPIWarningDescriptor, SomeDoc, SomeRow, Table } from '@/src/documents/index.js'; -import { Collection, DataAPIHttpError, DataAPIResponseError, DataAPITimeoutError } from '@/src/documents/index.js'; -import type { HTTPClientOptions, KeyspaceRef } from '@/src/lib/api/clients/types.js'; -import { HttpClient } from '@/src/lib/api/clients/http-client.js'; -import { HttpMethods } from '@/src/lib/api/constants.js'; -import type { CollectionOptions, TableOptions } from '@/src/db/index.js'; -import type { TimeoutManager } from '@/src/lib/api/timeouts/timeouts.js'; -import { Timeouts } from '@/src/lib/api/timeouts/timeouts.js'; -import type { EmptyObj } from '@/src/lib/types.js'; -import type { ParsedAdminOptions } from '@/src/client/opts-handlers/admin-opts-handler.js'; -import type { DbAdmin } from '@/src/administration/index.js'; -import { NonErrorError } from '@/src/lib/errors.js'; -import type { ParsedTokenProvider } from '@/src/lib/token-providers/token-provider.js'; -import type { DevOpsAPIRequestInfo } from '@/src/lib/api/clients/devops-api-http-client.js'; -import { isNonEmpty } from '@/src/lib/utils.js'; - -/** - * @internal - */ -type ClientKind = 'admin' | 'normal'; - -/** - * @internal - */ -type ExecCmdOpts = (Kind extends 'admin' ? { methodName: DevOpsAPIRequestInfo['methodName'] } : EmptyObj) & { - keyspace?: string | null, - timeoutManager: TimeoutManager, - bigNumsPresent?: boolean, - collection?: string, - table?: string, - extraLogInfo?: Record, -} - -/** - * @internal - */ -export interface DataAPIRequestInfo { - url: string, - tOrC: string | undefined, - tOrCType: 'table' | 'collection' | undefined, - keyspace: string | null, - command: Record, - timeoutManager: TimeoutManager, - bigNumsPresent: boolean | undefined, -} - -/** - * @internal - */ -type EmissionStrategy = (logger: InternalLogger) => { - emitCommandStarted?(requestId: string, info: DataAPIRequestInfo, opts: ExecCmdOpts): void, - emitCommandFailed?(requestId: string, info: DataAPIRequestInfo, resp: RawDataAPIResponse | undefined, error: Error, started: number, opts: ExecCmdOpts): void, - emitCommandSucceeded?(requestId: string, info: DataAPIRequestInfo, resp: RawDataAPIResponse, started: number, opts: ExecCmdOpts): void, - emitCommandWarnings?(requestId: string, info: DataAPIRequestInfo, warnings: NonEmpty, opts: ExecCmdOpts): void, -} - -/** - * @internal - */ -interface EmissionStrategies { - Normal: EmissionStrategy<'normal'>, - Admin: EmissionStrategy<'admin'>, -} - -/** - * @internal - */ -export const EmissionStrategy: EmissionStrategies = { - Normal: (logger) => ({ - emitCommandStarted(reqId, info, opts) { - logger.commandStarted?.(reqId, info, opts.extraLogInfo); - }, - emitCommandFailed(reqId, info, resp, error, started, opts) { - logger.commandFailed?.(reqId, info, opts.extraLogInfo, resp, error, started); - }, - emitCommandSucceeded(reqId, info, resp, started, opts) { - logger.commandSucceeded?.(reqId, info, opts.extraLogInfo, resp, started); - }, - emitCommandWarnings(reqId, info, warnings, opts) { - logger.commandWarnings?.(reqId, info, opts.extraLogInfo, warnings); - }, - }), - Admin: (logger) => ({ - emitCommandStarted(reqId, info, opts) { - logger.adminCommandStarted?.(reqId, '', adaptInfo4Devops(info, opts.methodName), true, null!); // TODO - }, - emitCommandFailed(reqId, info, _, error, started, opts) { - logger.adminCommandFailed?.(reqId, '', adaptInfo4Devops(info, opts.methodName), true, error, started); - }, - emitCommandSucceeded(reqId, info, resp, started, opts) { - logger.adminCommandSucceeded?.(reqId, '', adaptInfo4Devops(info, opts.methodName), true, resp, started); - }, - emitCommandWarnings(reqId, info, warnings, opts) { - logger.adminCommandWarnings?.(reqId, '', adaptInfo4Devops(info, opts.methodName), true, warnings); - }, - }), -}; - -const adaptInfo4Devops = (info: DataAPIRequestInfo, methodName: DevOpsAPIRequestInfo['methodName']) => ({ - method: 'POST', - data: info.command, - path: info.url, - methodName, -}); - -/** - * @internal - */ -interface DataAPIHttpClientOpts extends Omit { - keyspace: KeyspaceRef, - emissionStrategy: EmissionStrategy, - tokenProvider: ParsedTokenProvider, -} - -/** - * @internal - */ -export interface BigNumberHack { - parseWithBigNumbers(json: string): boolean, - parser: { - parse: (json: string) => SomeDoc, - stringify: (obj: SomeDoc) => string, - }, -} - -/** - * @internal - */ -export class DataAPIHttpClient extends HttpClient { - public collectionName?: string; - public tableName?: string; - public keyspace: KeyspaceRef; - public emissionStrategy: ReturnType>; - public bigNumHack?: BigNumberHack; - - readonly #props: DataAPIHttpClientOpts; - - constructor(opts: DataAPIHttpClientOpts) { - super('data-api', { - ...opts, - additionalHeaders: HeadersProvider.opts.fromObj.concat([ - opts.additionalHeaders, - opts.tokenProvider.toHeadersProvider(), - ]), - mkTimeoutError: DataAPITimeoutError.mk, - }); - - this.keyspace = opts.keyspace; - this.#props = opts; - this.emissionStrategy = opts.emissionStrategy(opts.logger.internal); - } - - public forTableSlashCollectionOrWhateverWeWouldCallTheUnionOfTheseTypes(tSlashC: Collection | Table, opts: CollectionOptions | TableOptions | undefined, bigNumHack: BigNumberHack): DataAPIHttpClient { - const clone = new DataAPIHttpClient({ - ...this.#props, - emissionStrategy: EmissionStrategy.Normal, - keyspace: { ref: tSlashC.keyspace }, - logger: tSlashC, - additionalHeaders: HeadersProvider.opts.monoid.concat([ - this.#props.additionalHeaders, - HeadersProvider.opts.fromStr(EmbeddingAPIKeyHeaderProvider).parse(opts?.embeddingApiKey), - HeadersProvider.opts.fromStr(RerankingAPIKeyHeaderProvider).parse(opts?.rerankingApiKey), - ]), - }); - - if (tSlashC instanceof Collection) { - clone.collectionName = tSlashC.name; - } else { - clone.tableName = tSlashC.name; - } - - clone.bigNumHack = bigNumHack; - clone.tm = new Timeouts(DataAPITimeoutError.mk, Timeouts.cfg.parse({ ...this.tm.baseTimeouts, ...opts?.timeoutDefaults })); - - return clone; - } - - public forDbAdmin(dbAdmin: DbAdmin, opts: ParsedAdminOptions): DataAPIHttpClient<'admin'> { - const clone = new DataAPIHttpClient({ - ...this.#props, - tokenProvider: TokenProvider.opts.concat([opts.adminToken, this.#props.tokenProvider]), - baseUrl: opts?.endpointUrl ?? this.#props.baseUrl, - baseApiPath: opts?.endpointUrl ? '' : this.#props.baseApiPath, - emissionStrategy: EmissionStrategy.Admin, - logger: dbAdmin, - }); - - clone.collectionName = undefined; - clone.tableName = undefined; - clone.tm = new Timeouts(DataAPITimeoutError.mk, { ...this.tm.baseTimeouts, ...opts?.timeoutDefaults }); - - return clone; - } - - public async executeCommand(command: Record, options: ExecCmdOpts): Promise { - if (options?.collection && options.table) { - throw new Error('Can\'t provide both `table` and `collection` as options to DataAPIHttpClient.executeCommand()'); - } - - const tOrC = options.collection || options.table || this.collectionName || this.tableName; - const keyspace = options.keyspace === undefined ? this.keyspace?.ref : options.keyspace; - - if (keyspace === undefined) { - throw new Error('Db is missing a required keyspace; be sure to set one with client.db(..., { keyspace }), or db.useKeyspace()'); - } - - if (keyspace === null && tOrC) { - throw new Error('Keyspace may not be `null` when a table or collection is provided to DataAPIHttpClient.executeCommand()'); - } - - const info: DataAPIRequestInfo = { - url: this.baseUrl, - tOrC: tOrC, - tOrCType: !tOrC ? undefined : tOrC === (options.table || this.tableName) ? 'table' : 'collection', - keyspace: keyspace, - command: command, - timeoutManager: options.timeoutManager, - bigNumsPresent: options.bigNumsPresent, - }; - - const keyspacePath = info.keyspace ? `/${info.keyspace}` : ''; - const collectionPath = info.tOrC ? `/${info.tOrC}` : ''; - info.url += keyspacePath + collectionPath; - - const requestId = this.logger.internal.generateCommandRequestId(); - - this.emissionStrategy.emitCommandStarted?.(requestId, info, options); - const started = performance.now(); - - let clonedData: RawDataAPIResponse | undefined; - - try { - const serialized = (info.bigNumsPresent) - ? this.bigNumHack?.parser.stringify(info.command) - : JSON.stringify(info.command); - - const resp = await this._request({ - url: info.url, - data: serialized, - timeoutManager: info.timeoutManager, - method: HttpMethods.Post, - }); - - if (resp.status >= 400 && resp.status !== 401) { - throw new DataAPIHttpError(resp); - } - - const data = (resp.body) - ? (this.bigNumHack?.parseWithBigNumbers(resp.body)) - ? this.bigNumHack?.parser.parse(resp.body) - : JSON.parse(resp.body) - /* c8 ignore next: exceptional case */ - : {}; - - clonedData = requestId - ? structuredClone(data) - : undefined; - - const warnings = data?.status?.warnings ?? []; - - if (warnings.length) { - this.emissionStrategy.emitCommandWarnings?.(requestId, info, warnings, options); - } - - if (data.errors && isNonEmpty(data.errors)) { - throw new DataAPIResponseError(info.command, data); - } - - const respData = { - data: data.data, - status: data.status, - errors: data.errors, - }; - - this.emissionStrategy.emitCommandSucceeded?.(requestId, info, clonedData!, started, options); - return respData; - } catch (thrown) { - const err = NonErrorError.asError(thrown); - this.emissionStrategy.emitCommandFailed?.(requestId, info, clonedData, err, started, options); - throw err; - } - } -} diff --git a/src/lib/api/clients/devops-api-http-client.ts b/src/lib/api/clients/devops-api-http-client.ts deleted file mode 100644 index 23e33d8d..00000000 --- a/src/lib/api/clients/devops-api-http-client.ts +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright DataStax, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// noinspection ExceptionCaughtLocallyJS - -import { HttpClient } from '@/src/lib/api/clients/index.js'; -import { - DevOpsAPIResponseError, - DevOpsAPITimeoutError, -} from '@/src/administration/errors.js'; -import type { AstraAdminBlockingOptions } from '@/src/administration/types/index.js'; -import { HttpMethods } from '@/src/lib/api/constants.js'; -import type { HTTPClientOptions, HttpMethodStrings } from '@/src/lib/api/clients/types.js'; -import { jsonTryParse } from '@/src/lib/utils.js'; -import type { TimeoutManager } from '@/src/lib/api/timeouts/timeouts.js'; -import { NonErrorError } from '@/src/lib/errors.js'; -import { HeadersProvider } from '@/src/lib/index.js'; -import type { ParsedTokenProvider } from '@/src/lib/token-providers/token-provider.js'; - -/** - * @internal - */ -export interface DevOpsAPIRequestInfo { - path: string, - method: HttpMethodStrings, - data?: Record, - params?: Record, - methodName: `${'admin' | 'dbAdmin'}.${string}`, -} - -/** - * @internal - */ -interface LongRunningRequestInfo { - id: string | ((resp: DevopsAPIResponse) => string), - target: string, - legalStates: string[], - defaultPollInterval: number, - options: AstraAdminBlockingOptions | undefined, - timeoutManager: TimeoutManager, -} - -/** - * @internal - */ -interface DevopsAPIResponse { - data?: Record, - headers: Record, - status: number, -} - -/** - * @internal - */ -interface DevOpsAPIHttpClientOpts extends Omit { - tokenProvider: ParsedTokenProvider, -} - -/** - * @internal - */ -export class DevOpsAPIHttpClient extends HttpClient { - constructor(opts: DevOpsAPIHttpClientOpts) { - super('devops-api', { - ...opts, - additionalHeaders: HeadersProvider.opts.fromObj.concat([ - opts.additionalHeaders, - opts.tokenProvider.toHeadersProvider(), - ]), - mkTimeoutError: DevOpsAPITimeoutError.mk, - }); - } - - public async request(req: DevOpsAPIRequestInfo, timeoutManager: TimeoutManager, started = 0): Promise { - return this._executeRequest(req, timeoutManager, started, this.logger.internal.generateAdminCommandRequestId()); - } - - public async requestLongRunning(req: DevOpsAPIRequestInfo, info: LongRunningRequestInfo): Promise { - const isLongRunning = info.options?.blocking !== false; - const timeoutManager = info.timeoutManager; - - const requestId = this.logger.internal.generateAdminCommandRequestId(); - - this.logger.internal.adminCommandStarted?.(requestId, this.baseUrl, req, isLongRunning, timeoutManager.initial()); - - const started = performance.now(); - const resp = await this._executeRequest(req, timeoutManager, started, requestId); - - const id = (typeof info.id === 'function') - ? info.id(resp) - : info.id; - - await this._awaitStatus(id, req, info, started, requestId); - - this.logger.internal.adminCommandSucceeded?.(requestId, this.baseUrl, req, isLongRunning, resp, started); - - return resp; - } - - private async _executeRequest(req: DevOpsAPIRequestInfo, timeoutManager: TimeoutManager, started: number, requestId: string): Promise { - const isLongRunning = started !== 0; - - try { - const url = this.baseUrl + req.path; - - if (!isLongRunning) { - this.logger.internal.adminCommandStarted?.(requestId, this.baseUrl, req, isLongRunning, timeoutManager.initial()); - } - - started ||= performance.now(); - - const resp = await this._request({ - url: url, - method: req.method, - params: req.params, - data: JSON.stringify(req.data), - forceHttp1: true, - timeoutManager, - }); - - const data = resp.body ? jsonTryParse(resp.body, undefined) : undefined; - - if (resp.status >= 400) { - throw new DevOpsAPIResponseError(resp, data); - } - - if (!isLongRunning) { - this.logger.internal.adminCommandSucceeded?.(requestId, this.baseUrl, req, false, data, started); - } - - return { - data: data, - status: resp.status, - headers: resp.headers, - }; - } catch (thrown) { - const err = NonErrorError.asError(thrown); - this.logger.internal.adminCommandFailed?.(requestId, this.baseUrl, req, isLongRunning, err, started); - throw err; - } - } - - private async _awaitStatus(id: string, req: DevOpsAPIRequestInfo, info: LongRunningRequestInfo, started: number, requestId: string): Promise { - if (info.options?.blocking === false) { - return; - } - - const pollInterval = info.options?.pollInterval || info.defaultPollInterval; - let waiting = false; - - for (let i = 1; i++;) { - /* c8 ignore next 3: exceptional case that can't be manually reproduced */ - if (waiting) { - continue; - } - waiting = true; - - this.logger.internal.adminCommandPolling?.(requestId, this.baseUrl, req, started, pollInterval, i); - - const resp = await this.request({ - method: HttpMethods.Get, - path: `/databases/${id}`, - methodName: req.methodName, - }, info.timeoutManager, started); - - if (resp.data?.status === info.target) { - break; - } - - /* c8 ignore start: exceptional case that can't be manually reproduced */ - if (!info.legalStates.includes(resp.data?.status)) { - const okStates = [info.target, ...info.legalStates]; - const error = new Error(`Created database is not in any legal state [${okStates.join(',')}]; current state: ${resp.data?.status}`); - - this.logger.internal.adminCommandFailed?.(requestId, this.baseUrl, req, true, error, started); - throw error; - } - /* c8 ignore end */ - - await new Promise((resolve) => { - setTimeout(() => { - waiting = false; - resolve(); - }, pollInterval); - }); - } - } -} diff --git a/src/lib/api/clients/http-client.ts b/src/lib/api/clients/http-client.ts deleted file mode 100644 index 49fea7c4..00000000 --- a/src/lib/api/clients/http-client.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright DataStax, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import type { FetchCtx, FetcherResponseInfo } from '@/src/lib/api/fetch/fetcher.js'; -import type { HTTPClientOptions, HTTPRequestInfo } from '@/src/lib/api/clients/index.js'; -import type { DataAPIClientEventMap } from '@/src/lib/logging/index.js'; -import { Timeouts } from '@/src/lib/api/timeouts/timeouts.js'; -import type { HierarchicalLogger } from '@/src/lib/index.js'; -import { HeadersResolver } from '@/src/lib/api/clients/headers-resolver.js'; - -/** - * @internal - */ -export abstract class HttpClient { - readonly baseUrl: string; - readonly logger: HierarchicalLogger; - readonly fetchCtx: FetchCtx; - readonly headersResolver: HeadersResolver; - tm: Timeouts; - - protected constructor(target: 'data-api' | 'devops-api', options: HTTPClientOptions) { - this.baseUrl = options.baseUrl; - this.logger = options.logger; - this.fetchCtx = options.fetchCtx; - - if (options.baseApiPath) { - this.baseUrl += '/' + options.baseApiPath; - } - - // this.baseHeaders = { ...options.additionalHeaders }; - // this.baseHeaders['User-Agent'] = options.caller.userAgent; - // this.baseHeaders['Content-Type'] = 'application/json'; - // - // this.headerProviders = headerProviders; - - this.headersResolver = new HeadersResolver(target, options.additionalHeaders, { - 'User-Agent': options.caller.userAgent, - 'Content-Type': 'application/json', - }); - - this.tm = new Timeouts(options.mkTimeoutError, options.timeoutDefaults); - } - - protected async _request(info: HTTPRequestInfo): Promise { - if (this.fetchCtx.closed.ref) { - throw new Error('Can\'t make requests on a closed client'); - } - - const [msRemaining, mkTimeoutError] = info.timeoutManager.advance(info); - - if (msRemaining <= 0) { - throw mkTimeoutError(); - } - - const params = info.params ?? {}; - - const url = (Object.keys(params).length > 0) - ? `${info.url}?${new URLSearchParams(params).toString()}` - : info.url; - - const maybePromiseHeaders = this.headersResolver.resolve(); - - const headers = (maybePromiseHeaders instanceof Promise) - ? await maybePromiseHeaders - : maybePromiseHeaders; - - return await this.fetchCtx.ctx.fetch({ - url: url, - body: info.data, - method: info.method, - headers: headers, - forceHttp1: !!info.forceHttp1, - timeout: msRemaining, - mkTimeoutError, - }); - } -} - diff --git a/src/lib/api/clients/impls/base-http-client.ts b/src/lib/api/clients/impls/base-http-client.ts new file mode 100644 index 00000000..c895621c --- /dev/null +++ b/src/lib/api/clients/impls/base-http-client.ts @@ -0,0 +1,159 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { FetchCtx, FetcherResponseInfo } from '@/src/lib/api/fetch/fetcher.js'; +import type { HttpMethodStrings } from '@/src/lib/api/clients/index.js'; +import type { TimeoutAdapter, TimeoutDescriptor } from '@/src/lib/api/timeouts/timeouts.js'; +import { type TimeoutManager, Timeouts } from '@/src/lib/api/timeouts/timeouts.js'; +import type { DataAPIClientEventMap, HierarchicalLogger } from '@/src/lib/index.js'; +import { type CommandOptions, HeadersProvider } from '@/src/lib/index.js'; +import type { HeadersResolverAdapter } from '@/src/lib/api/clients/utils/headers-resolver.js'; +import { HeadersResolver } from '@/src/lib/api/clients/utils/headers-resolver.js'; +import type { ParsedTimeoutDescriptor } from '@/src/lib/api/timeouts/cfg-handler.js'; +import type { ParsedCaller } from '@/src/client/opts-handlers/caller-cfg-handler.js'; +import type { ParsedHeadersProviders } from '@/src/lib/headers-providers/root/opts-handlers.js'; +import type { RetryAdapter } from '@/src/lib/api/retries/manager.js'; +import { RetryManager } from '@/src/lib/api/retries/manager.js'; +import type { ParsedTokenProvider } from '@/src/lib/token-providers/token-provider.js'; +import type { RetryContext } from '@/src/lib/api/retries/contexts/base.js'; +import { RequestId } from '@/src/lib/api/clients/utils/request-id.js'; +import { RetryCfgHandler } from '@/src/lib/api/retries/cfg-handler.js'; + +/** + * @internal + */ +export interface BaseHTTPClientOptions { + baseUrl: string, + logger: HierarchicalLogger, + fetchCtx: FetchCtx, + caller: ParsedCaller, + additionalHeaders: ParsedHeadersProviders, + timeoutDefaults: ParsedTimeoutDescriptor, + tokenProvider: ParsedTokenProvider, +} + +/** + * @internal + */ +export interface BaseExecuteOperationOptions { + timeoutManager: TimeoutManager, +} + +/** + * @internal + */ +export interface BaseRequestMetadata { + timeout: Partial, + requestId: RequestId, + startTime: number, +} + +/** + * @internal + */ +export interface HTTPRequestInfo { + url: string, + data?: string, + params?: Record, + method: HttpMethodStrings, + timeoutManager: TimeoutManager, + forceHttp1?: boolean, +} + +/** + * @internal + */ +export interface HttpClientAdapters { + retryAdapter: RetryAdapter, + headersResolverAdapter: HeadersResolverAdapter, + timeoutAdapter: TimeoutAdapter, +} + +/** + * @internal + */ +export abstract class BaseHttpClient { + protected readonly _baseUrl: string; + protected readonly _logger: HierarchicalLogger; + protected readonly _fetchCtx: FetchCtx; + protected readonly _headersResolver: HeadersResolver; + + public readonly tm: Timeouts; + public readonly rm: (isSafelyRetryable: boolean, opts: CommandOptions) => RetryManager; + + protected constructor(opts: BaseHTTPClientOptions, adapters: HttpClientAdapters) { + this._baseUrl = opts.baseUrl; + this._logger = opts.logger; + this._fetchCtx = opts.fetchCtx; + + const additionalHeaders = HeadersProvider.opts.fromObj.concat([ + opts.additionalHeaders, + opts.tokenProvider.toHeadersProvider(), + ]); + + this._headersResolver = new HeadersResolver(adapters.headersResolverAdapter, additionalHeaders, { + 'User-Agent': opts.caller.userAgent, + 'Content-Type': 'application/json', + }); + + this.tm = new Timeouts(adapters.timeoutAdapter, opts.timeoutDefaults); + + this.rm = (isSafelyRetryable: boolean, opts: CommandOptions) => { + return RetryManager.mk(isSafelyRetryable, opts, adapters.retryAdapter, RetryCfgHandler.empty); + }; + } + + protected _mkRequestMetadata(tm: TimeoutManager, metadata: Omit): Metadata { + return { + ...metadata, + requestId: new RequestId(), + startTime: performance.now(), + timeout: tm.initial(), + } as Metadata; + } + + protected async _request(info: HTTPRequestInfo): Promise { + if (this._fetchCtx.closed.ref) { + throw new Error('Can\'t make requests on a closed client'); + } + + const [msRemaining, mkTimeoutError] = info.timeoutManager.advance(info); + + if (msRemaining <= 0) { + throw mkTimeoutError(); + } + + const params = info.params ?? {}; + + const url = (Object.keys(params).length > 0) + ? `${info.url}?${new URLSearchParams(params).toString()}` + : info.url; + + const maybePromiseHeaders = this._headersResolver.resolve(); + + const headers = (maybePromiseHeaders instanceof Promise) + ? await maybePromiseHeaders + : maybePromiseHeaders; + + return await this._fetchCtx.ctx.fetch({ + url: url, + body: info.data, + method: info.method, + headers: headers, + forceHttp1: !!info.forceHttp1, + timeout: msRemaining, + mkTimeoutError, + }); + } +} diff --git a/src/lib/api/clients/impls/data-api-http-client.ts b/src/lib/api/clients/impls/data-api-http-client.ts new file mode 100644 index 00000000..3d73ab84 --- /dev/null +++ b/src/lib/api/clients/impls/data-api-http-client.ts @@ -0,0 +1,374 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// noinspection ExceptionCaughtLocallyJS + +import type { InternalLogger } from '@/src/lib/logging/internal-logger.js'; +import type { + DataAPIClientEventMap, + EmptyObj, + NonEmpty, + nullish, + RawDataAPIResponse, + TimedOutCategories, +} from '@/src/lib/index.js'; +import { + EmbeddingAPIKeyHeaderProvider, + HeadersProvider, + RerankingAPIKeyHeaderProvider, + TokenProvider, +} from '@/src/lib/index.js'; +import type { CommandEventTarget, DataAPIWarningDescriptor, SomeDoc, SomeRow, Table } from '@/src/documents/index.js'; +import { Collection, DataAPIHttpError, DataAPIResponseError, DataAPITimeoutError } from '@/src/documents/index.js'; +import type { KeyspaceRef } from '@/src/lib/api/clients/types.js'; +import type { + BaseExecuteOperationOptions, + BaseHTTPClientOptions, + BaseRequestMetadata, + HTTPRequestInfo, +} from '@/src/lib/api/clients/impls/base-http-client.js'; +import { BaseHttpClient } from '@/src/lib/api/clients/impls/base-http-client.js'; +import { HttpMethods } from '@/src/lib/api/constants.js'; +import type { CollectionOptions, TableOptions } from '@/src/db/index.js'; +import type { TimeoutAdapter } from '@/src/lib/api/timeouts/timeouts.js'; +import type { ParsedAdminOptions } from '@/src/client/opts-handlers/admin-opts-handler.js'; +import type { DbAdmin } from '@/src/administration/index.js'; +import { NonErrorError } from '@/src/lib/errors.js'; +import { isNonEmpty, isNullish } from '@/src/lib/utils.js'; +import { DataAPIRetryAdapter } from '@/src/lib/api/retries/adapters/data-api.js'; +import type { HeadersResolverAdapter } from '@/src/lib/api/clients/utils/headers-resolver.js'; +import type { DevOpsAPIRequestMetadata, ExecuteDevOpsAPIOperationOptions } from '@/src/lib/api/clients/index.js'; + +/** + * @internal + */ +type ClientKind = 'admin' | 'normal'; + +/** + * @internal + */ +export interface DataAPIHttpClientOpts extends BaseHTTPClientOptions { + keyspace: KeyspaceRef, + emissionStrategy: EmissionStrategy, + collection?: string, + table?: string, + bigNumHack?: BigNumberHack, +} + +/** + * @internal + */ +export type ExecuteDataAPICommandOptions = BaseExecuteOperationOptions & (Kind extends 'admin' ? { methodName: ExecuteDevOpsAPIOperationOptions['methodName'] } : EmptyObj) & { + keyspace?: string | null, + bigNumsPresent?: boolean, + collection?: string, + table?: string, + extraLogInfo?: Record, +} + +/** + * @internal + */ +export interface DataAPIRequestMetadata extends BaseRequestMetadata { + target: CommandEventTarget, + command: Record, + extra?: Record, +} + +/** + * @internal + */ +export interface BigNumberHack { + parseWithBigNumbers(json: string): boolean, + parser: { + parse: (json: string) => SomeDoc, + stringify: (obj: SomeDoc) => string, + }, +} + +/** + * @internal + */ +export class DataAPIHttpClient extends BaseHttpClient { + public target: InternalRequestTarget; + public emissionStrategy: ReturnType>; + public bigNumHack?: BigNumberHack; + + readonly #baseOpts: DataAPIHttpClientOpts; + + constructor(opts: DataAPIHttpClientOpts) { + super(opts, { + retryAdapter: new DataAPIRetryAdapter(opts.logger), + headersResolverAdapter: DataAPIHeadersResolverAdapter, + timeoutAdapter: DataAPITimeoutAdapter, + }); + + this.target = new InternalRequestTarget(opts); + this.emissionStrategy = opts.emissionStrategy(opts.logger.internal, this._baseUrl); + this.bigNumHack = opts.bigNumHack; + this.#baseOpts = opts; + } + + public forTableSlashCollectionOrWhateverWeWouldCallTheUnionOfTheseTypes(tSlashC: Collection | Table, opts: CollectionOptions | TableOptions | undefined, bigNumHack: BigNumberHack): DataAPIHttpClient { + return new DataAPIHttpClient({ + ...this.#baseOpts, + emissionStrategy: EmissionStrategy.Normal, + keyspace: { ref: tSlashC.keyspace }, + logger: tSlashC, + additionalHeaders: HeadersProvider.opts.monoid.concat([ + this.#baseOpts.additionalHeaders, + HeadersProvider.opts.fromStr(EmbeddingAPIKeyHeaderProvider).parse(opts?.embeddingApiKey), + HeadersProvider.opts.fromStr(RerankingAPIKeyHeaderProvider).parse(opts?.rerankingApiKey), + ]), + timeoutDefaults: { + ...this.#baseOpts.timeoutDefaults, + ...opts?.timeoutDefaults, + }, + collection: tSlashC instanceof Collection ? tSlashC.name : undefined, + table: tSlashC instanceof Collection ? undefined : tSlashC.name, + bigNumHack, + }); + } + + public forDbAdmin(dbAdmin: DbAdmin, opts: ParsedAdminOptions): DataAPIHttpClient<'admin'> { + return new DataAPIHttpClient({ + ...this.#baseOpts, + tokenProvider: TokenProvider.opts.concat([opts.adminToken, this.#baseOpts.tokenProvider]), + baseUrl: opts?.endpointUrl ?? this.#baseOpts.baseUrl, + emissionStrategy: EmissionStrategy.Admin, + logger: dbAdmin, + timeoutDefaults: { + ...this.#baseOpts.timeoutDefaults, + ...opts?.timeoutDefaults, + }, + collection: undefined, + table: undefined, + }); + } + + public async executeCommand(command: Record, opts: ExecuteDataAPICommandOptions): Promise { + const metadata = this._mkRequestMetadata(opts.timeoutManager, { + target: this.target.forRequest(opts), + extra: opts.extraLogInfo, + command: command, + }); + + this.emissionStrategy.emitCommandStarted(metadata, opts); + + let clonedData: RawDataAPIResponse | undefined; + + try { + const serialized = (opts.bigNumsPresent) + ? this.bigNumHack?.parser.stringify(metadata.command) + : JSON.stringify(metadata.command); + + const resp = await this._request({ + url: metadata.target.url, + data: serialized, + timeoutManager: opts.timeoutManager, + method: HttpMethods.Post, + }); + + if (resp.status >= 400 && resp.status !== 401) { + throw new DataAPIHttpError(resp); + } + + const data = (resp.body) + ? (this.bigNumHack?.parseWithBigNumbers(resp.body)) + ? this.bigNumHack?.parser.parse(resp.body) + : JSON.parse(resp.body) + /* c8 ignore next: exceptional case */ + : {}; + + clonedData = structuredClone(data); + + const warnings = data?.status?.warnings ?? []; + + if (warnings.length) { + this.emissionStrategy.emitCommandWarnings(metadata, warnings, opts); + } + + if (data.errors && isNonEmpty(data.errors)) { + throw new DataAPIResponseError(metadata.command, data); + } + + const respData = { + data: data.data, + status: data.status, + errors: data.errors, + }; + + this.emissionStrategy.emitCommandSucceeded(metadata, clonedData!, opts); + return respData; + } catch (thrown) { + const err = NonErrorError.asError(thrown); + this.emissionStrategy.emitCommandFailed(metadata, clonedData, err, opts); + throw err; + } + } +} + +/** + * @internal + */ +class InternalRequestTarget { + private _cached: CommandEventTarget; + + private readonly _baseUrl: string; + private readonly _keyspace: KeyspaceRef; + + constructor(opts: DataAPIHttpClientOpts) { + this._baseUrl = opts.baseUrl; + this._keyspace = opts.keyspace; + this._cached = this._buildCommandEventTarget(this._keyspace.ref, opts.collection, opts.table); + } + + public forRequest(opts: ExecuteDataAPICommandOptions): Readonly { + this._rebuildCacheIfKeyspaceChanged(); + + const keyspace = opts.keyspace === undefined ? this._cached.keyspace : opts.keyspace; + + if (keyspace === undefined) { + throw new Error('Db is missing a working keyspace; set one with client.db(..., { keyspace }) or db.useKeyspace()'); + } + + if (keyspace === this._cached.keyspace) { + if ((!opts.collection && !opts.table) || (opts.collection === this._cached.collection && opts.table === this._cached.table)) { + return this._cached; + } + } + + return this._buildCommandEventTarget(keyspace, opts.collection, opts.table); + } + + private _buildCommandEventTarget(keyspace: string | nullish, coll: string | undefined, table: string | undefined) { + if (coll && table) { + throw new Error('Can\'t provide both `table` and `collection` as options to DataAPIHttpClient.executeCommand()'); + } + + const tOrC = coll || table || this._cached?.collection || this._cached?.table; + + const keyspacePath = keyspace ? `/${keyspace}` : ''; + const collectionPath = tOrC ? `/${tOrC}` : ''; + + const target = { + url: this._baseUrl + keyspacePath + collectionPath, + } as CommandEventTarget; + + if (!isNullish(keyspace)) { + target.keyspace = keyspace; + + if (tOrC) { + if (tOrC === coll || tOrC === this._cached?.collection) { + target.collection = tOrC; + } else { + target.table = tOrC; + } + } + } else if (tOrC) { + throw new Error('Keyspace may not be `null` when a table or collection is provided to DataAPIHttpClient.executeCommand()'); + } + + return target; + } + + private _rebuildCacheIfKeyspaceChanged() { + if (this._keyspace.ref !== this._cached.keyspace) { + this._cached = this._buildCommandEventTarget(this._keyspace.ref, this._cached.collection, this._cached.table); + } + } +} + +/** + * @internal + */ +const DataAPIHeadersResolverAdapter: HeadersResolverAdapter = { + target: 'data-api', +}; + +/** + * @internal + */ +const DataAPITimeoutAdapter: TimeoutAdapter = { + mkTimeoutError(info: HTTPRequestInfo, categories: TimedOutCategories): Error { + return new DataAPITimeoutError(info, categories); + }, +}; + +/** + * @internal + */ +type EmissionStrategy = (logger: InternalLogger, baseUrl: string) => { + emitCommandStarted(info: DataAPIRequestMetadata, opts: ExecuteDataAPICommandOptions): void, + emitCommandFailed(info: DataAPIRequestMetadata, resp: RawDataAPIResponse | undefined, error: Error, opts: ExecuteDataAPICommandOptions): void, + emitCommandSucceeded(info: DataAPIRequestMetadata, resp: RawDataAPIResponse, opts: ExecuteDataAPICommandOptions): void, + emitCommandWarnings(info: DataAPIRequestMetadata, warnings: NonEmpty, opts: ExecuteDataAPICommandOptions): void, +} + +/** + * @internal + */ +interface EmissionStrategies { + Normal: EmissionStrategy<'normal'>, + Admin: EmissionStrategy<'admin'>, +} + +/** + * @internal + */ +export const EmissionStrategy: EmissionStrategies = { + Normal: (logger) => ({ + emitCommandStarted(metadata) { + logger.commandStarted?.(metadata); + }, + emitCommandFailed(metadata, resp, error) { + logger.commandFailed?.(metadata, resp, error); + }, + emitCommandSucceeded(metadata, resp) { + logger.commandSucceeded?.(metadata, resp); + }, + emitCommandWarnings(metadata, warnings) { + logger.commandWarnings?.(metadata, warnings); + }, + }), + Admin: (logger, baseUrl) => ({ + emitCommandStarted(metadata, opts) { + logger.adminCommandStarted?.(adaptInfo4Devops(baseUrl, metadata, opts)); + }, + emitCommandFailed(metadata, _, error, opts) { + logger.adminCommandFailed?.(adaptInfo4Devops(baseUrl, metadata, opts), error); + }, + emitCommandSucceeded(metadata, resp, opts) { + logger.adminCommandSucceeded?.(adaptInfo4Devops(baseUrl, metadata, opts), resp); + }, + emitCommandWarnings(metadata, warnings, opts) { + logger.adminCommandWarnings?.(adaptInfo4Devops(baseUrl, metadata, opts), warnings); + }, + }), +}; + +const adaptInfo4Devops = (baseUrl: string, metadata: DataAPIRequestMetadata, opts: ExecuteDataAPICommandOptions<'admin'>): DevOpsAPIRequestMetadata => { + timeout: metadata.timeout, + requestId: metadata.requestId, + startTime: metadata.startTime, + baseUrl: baseUrl, + isLongRunning: false, + methodName: opts.methodName, + reqOpts: { + timeoutManager: opts.timeoutManager, + data: metadata.command, + path: metadata.target.url.replace(baseUrl, ''), + method: HttpMethods.Post, + methodName: opts.methodName, + }, +}; diff --git a/src/lib/api/clients/impls/devops-api-http-client.ts b/src/lib/api/clients/impls/devops-api-http-client.ts new file mode 100644 index 00000000..404b08c1 --- /dev/null +++ b/src/lib/api/clients/impls/devops-api-http-client.ts @@ -0,0 +1,213 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// noinspection ExceptionCaughtLocallyJS + +import type { + BaseExecuteOperationOptions, + BaseHTTPClientOptions, + BaseRequestMetadata, + HttpMethodStrings, +} from '@/src/lib/api/clients/index.js'; +import { BaseHttpClient, type HTTPRequestInfo } from '@/src/lib/api/clients/index.js'; +import { DevOpsAPIResponseError, DevOpsAPITimeoutError } from '@/src/administration/errors.js'; +import type { AstraAdminBlockingOptions } from '@/src/administration/types/index.js'; +import { HttpMethods } from '@/src/lib/api/constants.js'; +import { jsonTryParse } from '@/src/lib/utils.js'; +import type { TimedOutCategories, TimeoutAdapter } from '@/src/lib/api/timeouts/timeouts.js'; +import { NonErrorError } from '@/src/lib/errors.js'; +import { DevOpsAPIRetryAdapter } from '@/src/lib/api/retries/adapters/devops-api.js'; +import type { HeadersResolverAdapter } from '@/src/lib/api/clients/utils/headers-resolver.js'; + +/** + * @internal + */ +export type DevOpsAPIHttpClientOpts = BaseHTTPClientOptions + +/** + * @internal + */ +export interface ExecuteDevOpsAPIOperationOptions extends BaseExecuteOperationOptions { + path: string, + method: HttpMethodStrings, + data?: Record, + params?: Record, + methodName: `${'admin' | 'dbAdmin'}.${string}`, +} + +/** + * @internal + */ +export interface LongRunningOperationOptions { + id: string | ((resp: RawDevOpsAPIResponse) => string), + target: string, + legalStates: string[], + defaultPollInterval: number, + options: AstraAdminBlockingOptions | undefined, +} + +/** + * @internal + */ +export interface DevOpsAPIRequestMetadata extends BaseRequestMetadata { + reqOpts: ExecuteDevOpsAPIOperationOptions, + baseUrl: string, + isLongRunning: boolean, + methodName: string, +} + +/** + * @internal + */ +export interface RawDevOpsAPIResponse { + data?: Record, + headers: Record, + status: number, +} + +/** + * @internal + */ +export class DevOpsAPIHttpClient extends BaseHttpClient { + constructor(opts: DevOpsAPIHttpClientOpts) { + super(opts, { + retryAdapter: new DevOpsAPIRetryAdapter(opts.logger), + headersResolverAdapter: DevOpsAPIHeadersResolverAdapter, + timeoutAdapter: DevOpsAPITimeoutAdapter, + }); + } + + public async request(opts: ExecuteDevOpsAPIOperationOptions): Promise { + return this._executeOperation(opts); + } + + public async requestLongRunning(opts: ExecuteDevOpsAPIOperationOptions, lrInfo: LongRunningOperationOptions): Promise { + return this._executeOperation(opts, lrInfo); + } + + private async _executeOperation(opts: ExecuteDevOpsAPIOperationOptions, lrInfo?: LongRunningOperationOptions): Promise { + const metadata = this._mkRequestMetadata(opts.timeoutManager, { + baseUrl: this._baseUrl, + isLongRunning: !!lrInfo && lrInfo.options?.blocking !== false, + methodName: opts.methodName, + reqOpts: opts, + }); + + this._logger.internal.adminCommandStarted?.(metadata); + + try { + const resp = await this._makeRequest(opts); + + if (metadata.isLongRunning) { + const id = (typeof lrInfo!.id === 'function') + ? lrInfo!.id(resp) + : lrInfo!.id; + + await this._awaitStatus(id, opts, lrInfo!, metadata); + } + + this._logger.internal.adminCommandSucceeded?.(metadata, resp.data); + + return resp; + } catch (thrown) { + const err = NonErrorError.asError(thrown); + this._logger.internal.adminCommandFailed?.(metadata, err); + throw err; + } + } + + private async _awaitStatus(id: string, opts: ExecuteDevOpsAPIOperationOptions, lrInfo: LongRunningOperationOptions, metadata: DevOpsAPIRequestMetadata) { + if (lrInfo.options?.blocking === false) { + return; + } + + const pollInterval = lrInfo.options?.pollInterval || lrInfo.defaultPollInterval; + let waiting = false; + + for (let i = 1; i++;) { + /* c8 ignore next 3: exceptional case that can't be manually reproduced */ + if (waiting) { + continue; + } + waiting = true; + + this._logger.internal.adminCommandPolling?.(metadata, pollInterval, i); + + const resp = await this._makeRequest({ + method: HttpMethods.Get, + path: `/databases/${id}`, + methodName: opts.methodName, + timeoutManager: opts.timeoutManager, + }); + + if (resp.data?.status === lrInfo.target) { + break; + } + + /* c8 ignore start: exceptional case that can't be manually reproduced */ + if (!lrInfo.legalStates.includes(resp.data?.status)) { + const okStates = [lrInfo.target, ...lrInfo.legalStates]; + throw new Error(`Created database is not in any legal state [${okStates.join(',')}]; current state: ${resp.data?.status}`); + } + /* c8 ignore end */ + + await new Promise((resolve) => { + setTimeout(() => { + waiting = false; + resolve(); + }, pollInterval); + }); + } + } + + private async _makeRequest(opts: ExecuteDevOpsAPIOperationOptions): Promise { + const url = this._baseUrl + opts.path; + + const resp = await this._request({ + url: url, + method: opts.method, + params: opts.params, + data: JSON.stringify(opts.data), + forceHttp1: true, + timeoutManager: opts.timeoutManager, + }); + + const data = resp.body ? jsonTryParse(resp.body, undefined) : undefined; + + if (resp.status >= 400) { + throw new DevOpsAPIResponseError(resp, data); + } + + return { + data: data, + status: resp.status, + headers: resp.headers, + }; + } +} + +/** + * @internal + */ +const DevOpsAPIHeadersResolverAdapter: HeadersResolverAdapter = { + target: 'devops-api', +}; + +/** + * @internal + */ +const DevOpsAPITimeoutAdapter: TimeoutAdapter = { + mkTimeoutError(info: HTTPRequestInfo, categories: TimedOutCategories): Error { + return new DevOpsAPITimeoutError(info, categories); + }, +}; diff --git a/src/lib/api/clients/index.ts b/src/lib/api/clients/index.ts index ba7f5925..0c8c0215 100644 --- a/src/lib/api/clients/index.ts +++ b/src/lib/api/clients/index.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -export * from './devops-api-http-client.js'; -export * from './data-api-http-client.js'; -export * from './http-client.js'; +export * from './impls/devops-api-http-client.js'; +export * from './impls/data-api-http-client.js'; +export * from './impls/base-http-client.js'; export type * from './types.js'; diff --git a/src/lib/api/clients/types.ts b/src/lib/api/clients/types.ts index 2fb62374..463104be 100644 --- a/src/lib/api/clients/types.ts +++ b/src/lib/api/clients/types.ts @@ -12,28 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import type { FetchCtx } from '@/src/lib/api/fetch/fetcher.js'; import type { HttpMethods } from '@/src/lib/api/constants.js'; import type { Ref } from '@/src/lib/types.js'; -import type { MkTimeoutError, TimeoutManager } from '@/src/lib/api/timeouts/timeouts.js'; -import type { ParsedTimeoutDescriptor } from '@/src/lib/api/timeouts/cfg-handler.js'; -import type { ParsedCaller } from '@/src/client/opts-handlers/caller-cfg-handler.js'; -import type { DataAPIClientEventMap, HierarchicalLogger } from '@/src/lib/index.js'; -import type { ParsedHeadersProviders } from '@/src/lib/headers-providers/root/opts-handlers.js'; - -/** - * @internal - */ -export interface HTTPClientOptions { - baseUrl: string, - baseApiPath?: string | null, - logger: HierarchicalLogger, - fetchCtx: FetchCtx, - caller: ParsedCaller, - additionalHeaders: ParsedHeadersProviders, - timeoutDefaults: ParsedTimeoutDescriptor, - mkTimeoutError: MkTimeoutError, -} /** * @internal @@ -44,15 +24,3 @@ export type HttpMethodStrings = typeof HttpMethods[keyof typeof HttpMethods]; * @internal */ export type KeyspaceRef = Ref; - -/** - * @internal - */ -export interface HTTPRequestInfo { - url: string, - data?: string, - params?: Record, - method: HttpMethodStrings, - timeoutManager: TimeoutManager, - forceHttp1?: boolean, -} diff --git a/src/lib/api/clients/headers-resolver.ts b/src/lib/api/clients/utils/headers-resolver.ts similarity index 75% rename from src/lib/api/clients/headers-resolver.ts rename to src/lib/api/clients/utils/headers-resolver.ts index 0b51a078..6bbaa1c9 100644 --- a/src/lib/api/clients/headers-resolver.ts +++ b/src/lib/api/clients/utils/headers-resolver.ts @@ -15,19 +15,23 @@ import type { ParsedHeadersProviders } from '@/src/lib/headers-providers/root/opts-handlers.js'; import { type GetHeadersCtx, HeadersProvider, PureHeadersProvider } from '@/src/lib/index.js'; +export interface HeadersResolverAdapter { + target: GetHeadersCtx['for']; +} + /** * @internal */ export class HeadersResolver { private readonly _resolveStrategy: StaticHeadersResolveStrategy | DynamicHeadersResolveStrategy; - constructor(target: 'data-api' | 'devops-api', additionalHeaders: ParsedHeadersProviders, baseHeaders: Record) { - const queue = this._mkResolveQueue(target, additionalHeaders); + constructor(adapter: HeadersResolverAdapter, additionalHeaders: ParsedHeadersProviders, baseHeaders: Record) { + const queue = this._mkResolveQueue(adapter, additionalHeaders); if (queue.length <= 1 && !(queue[0] instanceof HeadersProvider)) { this._resolveStrategy = new StaticHeadersResolveStrategy({ ...baseHeaders, ...queue[0] }); } else { - this._resolveStrategy = new DynamicHeadersResolveStrategy(target, baseHeaders, queue); + this._resolveStrategy = new DynamicHeadersResolveStrategy(adapter.target, baseHeaders, queue); } } @@ -35,28 +39,27 @@ export class HeadersResolver { return this._resolveStrategy.resolve(); } - private _mkResolveQueue(target: 'data-api' | 'devops-api', headerProviders: ParsedHeadersProviders) { - const ctx: GetHeadersCtx = { for: target }; + private _mkResolveQueue(adapter: HeadersResolverAdapter, headerProviders: ParsedHeadersProviders) { + const ctx: GetHeadersCtx = { for: adapter.target }; const ret = [] as (Record | HeadersProvider)[]; - let acc = {} as Record; + let staticAcc = {} as Record; for (const provider of headerProviders.providers) { // noinspection SuspiciousTypeOfGuard -- the lsp is wrong here if (provider instanceof PureHeadersProvider) { - assignNonUndefined(acc, provider.getHeaders(ctx)); + assignNonUndefined(staticAcc, provider.getHeaders(ctx)); } else { - ret.push(acc); - acc = {}; + ret.push(staticAcc); ret.push(provider); + staticAcc = {}; } } + ret.push(staticAcc); - if (Object.keys(acc).length > 0) { - ret.push(acc); - } - - return ret; + return ret.filter((obj) => { + return (Object.keys(obj).length > 0) || (obj instanceof HeadersProvider); + }); } } @@ -76,7 +79,7 @@ class StaticHeadersResolveStrategy { */ class DynamicHeadersResolveStrategy { constructor( - private readonly _target: 'data-api' | 'devops-api', + private readonly _target: GetHeadersCtx['for'], private readonly _baseHeaders: Record, private readonly _resolveQueue: (Record | HeadersProvider)[], ) {} diff --git a/src/lib/api/clients/utils/request-id.ts b/src/lib/api/clients/utils/request-id.ts new file mode 100644 index 00000000..ed94681f --- /dev/null +++ b/src/lib/api/clients/utils/request-id.ts @@ -0,0 +1,26 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as uuid from 'uuid'; + +/** + * @internal + */ +export class RequestId { + private _id?: string; + + get unwrap() { + return (this._id ??= uuid.v4()); + } +} diff --git a/src/lib/api/retries/adapters/data-api.ts b/src/lib/api/retries/adapters/data-api.ts new file mode 100644 index 00000000..e1d49645 --- /dev/null +++ b/src/lib/api/retries/adapters/data-api.ts @@ -0,0 +1,42 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { RetryAdapter } from '@/src/lib/api/retries/manager.js'; +import { DataAPIRetryContext } from '@/src/lib/api/retries/contexts/data-api.js'; +import type { DataAPIRequestMetadata } from '@/src/lib/api/clients/index.js'; +import type { InternalRetryContext } from '@/src/lib/api/retries/contexts/internal.js'; +import { type CommandEventMap, DataAPITimeoutError } from '@/src/documents/index.js'; +import type { HierarchicalLogger } from '@/src/lib/index.js'; + +/** + * @internal + */ +export class DataAPIRetryAdapter implements RetryAdapter { + public readonly policy = 'dataAPIPolicy'; + public readonly TimeoutError = DataAPITimeoutError; + + public constructor(private readonly _logger: HierarchicalLogger) {} + + public mkEphemeralCtx(ctx: InternalRetryContext, duration: number, error: Error, req: DataAPIRequestMetadata): DataAPIRetryContext { + return new DataAPIRetryContext(ctx, duration, error, req); + } + + public emitRetryEvent(): void { + // TODO + } + + public emitRetryDeclinedEvent(): void { + // TODO + } +} diff --git a/src/lib/api/retries/adapters/devops-api.ts b/src/lib/api/retries/adapters/devops-api.ts new file mode 100644 index 00000000..5b92684e --- /dev/null +++ b/src/lib/api/retries/adapters/devops-api.ts @@ -0,0 +1,43 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { RetryAdapter } from '@/src/lib/api/retries/manager.js'; +import type { DevOpsAPIRequestMetadata } from '@/src/lib/api/clients/index.js'; +import type { InternalRetryContext } from '@/src/lib/api/retries/contexts/internal.js'; +import type { HierarchicalLogger } from '@/src/lib/index.js'; +import type { AdminCommandEventMap } from '@/src/administration/index.js'; +import { DevOpsAPITimeoutError } from '@/src/administration/index.js'; +import { DevOpsAPIRetryContext } from '@/src/lib/api/retries/contexts/devops-api.js'; + +/** + * @internal + */ +export class DevOpsAPIRetryAdapter implements RetryAdapter { + public readonly policy = 'devOpsAPIPolicy'; + public readonly TimeoutError = DevOpsAPITimeoutError; + + public constructor(private readonly _logger: HierarchicalLogger) {} + + public mkEphemeralCtx(ctx: InternalRetryContext, duration: number, error: Error, metadata: DevOpsAPIRequestMetadata): DevOpsAPIRetryContext { + return new DevOpsAPIRetryContext(ctx, duration, error, metadata); + } + + public emitRetryEvent(): void { + // TODO + } + + public emitRetryDeclinedEvent(): void { + // TODO + } +} diff --git a/src/lib/api/retries/cfg-handler.ts b/src/lib/api/retries/cfg-handler.ts new file mode 100644 index 00000000..53b5e094 --- /dev/null +++ b/src/lib/api/retries/cfg-handler.ts @@ -0,0 +1,74 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { OptionsHandlerTypes, Parsed } from '@/src/lib/opts-handlers.js'; +import { MonoidalOptionsHandler, monoids } from '@/src/lib/opts-handlers.js'; +import { either, exact, nullish, optional } from 'decoders'; +import type { RetryConfig, ExplicitRetryConfig } from '@/src/lib/api/retries/config.js'; +import { RetryPolicy } from '@/src/lib/api/retries/policy.js'; +import type { RetryContext } from '@/src/lib/api/retries/contexts/base.js'; +import type { DataAPIRetryContext } from '@/src/lib/api/retries/contexts/data-api.js'; +import type { DevOpsAPIRetryContext } from '@/src/lib/api/retries/contexts/devops-api.js'; +import { anyInstanceOf } from '@/src/lib/utils.js'; + +/** + * @internal + */ +interface Type extends OptionsHandlerTypes { + Parsed: ParsedRetryConfig, + Parseable: RetryConfig | undefined | null, +} + +/** + * @internal + */ +export type ParsedRetryConfig = ExplicitRetryConfig & Parsed<'RetryConfig'>; + +const monoid = monoids.object({ + defaultPolicy: monoids.optional>(), + dataAPIPolicy: monoids.optional>(), + devOpsAPIPolicy: monoids.optional>(), +}); + +/** + * @internal + */ +const decoder = nullish(either( + anyInstanceOf(RetryPolicy), + exact({ + defaultPolicy: optional(anyInstanceOf(RetryPolicy)), + dataAPIPolicy: optional(anyInstanceOf(RetryPolicy)), + devOpsAPIPolicy: optional(anyInstanceOf(RetryPolicy)), + }), +), {}); + +/** + * @internal + */ +const transformer = decoder.transform((config) => { + if (!config) { + return monoid.empty; + } + + if (config instanceof RetryPolicy) { + return { defaultPolicy: config }; + } + + return config; +}); + +/** + * @internal + */ +export const RetryCfgHandler = new MonoidalOptionsHandler(transformer, monoid); diff --git a/src/lib/api/retries/config.ts b/src/lib/api/retries/config.ts new file mode 100644 index 00000000..3bd66974 --- /dev/null +++ b/src/lib/api/retries/config.ts @@ -0,0 +1,26 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { RetryPolicy } from '@/src/lib/api/retries/policy.js'; +import type { RetryContext } from '@/src/lib/api/retries/contexts/base.js'; +import type { DataAPIRetryContext } from '@/src/lib/api/retries/contexts/data-api.js'; +import type { DevOpsAPIRetryContext } from '@/src/lib/api/retries/contexts/devops-api.js'; + +export type RetryConfig = ExplicitRetryConfig | RetryPolicy; + +export interface ExplicitRetryConfig { + defaultPolicy?: RetryPolicy, + dataAPIPolicy?: RetryPolicy, + devOpsAPIPolicy?: RetryPolicy, +} diff --git a/src/lib/api/retries/contexts/base.ts b/src/lib/api/retries/contexts/base.ts new file mode 100644 index 00000000..1833f058 --- /dev/null +++ b/src/lib/api/retries/contexts/base.ts @@ -0,0 +1,51 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { TimeoutDescriptor } from '@/src/lib/index.js'; +import type { InternalRetryContext } from '@/src/lib/api/retries/contexts/internal.js'; +import type { DataAPIRetryContext } from '@/src/lib/api/retries/contexts/data-api.js'; +import type { DevOpsAPIRetryContext } from '@/src/lib/api/retries/contexts/devops-api.js'; + +export abstract class RetryContext { + public declare abstract readonly permits: DataAPIRetryContext | DevOpsAPIRetryContext; + + public readonly retryCount: number; + + public readonly error: Error; + + public readonly isSafelyRetryable: boolean; + + public readonly timeout: Partial; + + public readonly duration: number; + + public readonly userData: Record; + + public readonly requestId: string; + + /** + * Should not be instantiated by the user directly. + * + * @internal + */ + protected constructor(ctx: InternalRetryContext, duration: number, error: Error) { + this.retryCount = ctx.retryCount; + this.isSafelyRetryable = ctx.isSafelyRetryable; + this.timeout = ctx.timeout; + this.userData = ctx.userData; + this.duration = duration; + this.error = error; + this.requestId = ctx.requestId.unwrap; + } +} diff --git a/src/lib/api/retries/contexts/data-api.ts b/src/lib/api/retries/contexts/data-api.ts new file mode 100644 index 00000000..1e2a85ec --- /dev/null +++ b/src/lib/api/retries/contexts/data-api.ts @@ -0,0 +1,40 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { RetryContext } from '@/src/lib/api/retries/contexts/base.js'; +import type { InternalRetryContext } from '@/src/lib/api/retries/contexts/internal.js'; +import type { DataAPIRequestMetadata } from '@/src/lib/api/clients/index.js'; +import type { CommandEventTarget } from '@/src/documents/index.js'; + +export class DataAPIRetryContext extends RetryContext { + public declare readonly permits: this; + + public readonly target: CommandEventTarget; + + public readonly command: Record; + + public readonly commandName: string; + + /** + * Should not be instantiated by the user directly. + * + * @internal + */ + public constructor(ctx: InternalRetryContext, duration: number, error: Error, req: DataAPIRequestMetadata) { + super(ctx, duration, error); + this.target = req.target; + this.command = req.command; + this.commandName = Object.keys(req.command)[0]; + } +} diff --git a/src/lib/api/retries/contexts/devops-api.ts b/src/lib/api/retries/contexts/devops-api.ts new file mode 100644 index 00000000..30efef2c --- /dev/null +++ b/src/lib/api/retries/contexts/devops-api.ts @@ -0,0 +1,43 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { RetryContext } from '@/src/lib/api/retries/contexts/base.js'; +import type { InternalRetryContext } from '@/src/lib/api/retries/contexts/internal.js'; +import type { DevOpsAPIRequestMetadata, HttpMethodStrings } from '@/src/lib/api/clients/index.js'; +import { EqualityProof } from '@/src/lib/utils.js'; + +export class DevOpsAPIRetryContext extends RetryContext { + public declare readonly permits: this; + + public readonly method: 'GET' | 'POST' | 'DELETE'; + + public readonly methodName: string; + + public readonly path: string; + + /** + * Should not be instantiated by the user directly. + * + * @internal + */ + public constructor(ctx: InternalRetryContext, duration: number, error: Error, metadata: DevOpsAPIRequestMetadata) { + super(ctx, duration, error); + this.method = metadata.reqOpts.method; + this.methodName = metadata.methodName; + this.path = metadata.reqOpts.path; + } +} + +// ensures that `method` is correctly typed without actually exposing `HttpMethodStrings` +void EqualityProof; diff --git a/src/lib/api/retries/contexts/internal.ts b/src/lib/api/retries/contexts/internal.ts new file mode 100644 index 00000000..cff65531 --- /dev/null +++ b/src/lib/api/retries/contexts/internal.ts @@ -0,0 +1,37 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { TimeoutDescriptor } from '@/src/lib/index.js'; +import type { RequestId } from '@/src/lib/api/clients/utils/request-id.js'; + +/** + * @internal + */ +export class InternalRetryContext { + public retryCount = 0; + + public readonly isSafelyRetryable: boolean; + + public readonly timeout: Partial; + + public readonly userData: Record = {}; + + public readonly requestId: RequestId; + + public constructor(isSafelyRetryable: boolean, timeout: Partial, reqId: RequestId) { + this.isSafelyRetryable = isSafelyRetryable; + this.timeout = timeout; + this.requestId = reqId; + } +} diff --git a/src/lib/api/retries/manager.ts b/src/lib/api/retries/manager.ts new file mode 100644 index 00000000..7fa5eeef --- /dev/null +++ b/src/lib/api/retries/manager.ts @@ -0,0 +1,190 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { RetryContext } from '@/src/lib/api/retries/contexts/base.js'; +import type { CommandOptions, SomeConstructor } from '@/src/lib/index.js'; +import { InternalRetryContext } from '@/src/lib/api/retries/contexts/internal.js'; +import type { TimeoutManager } from '@/src/lib/api/timeouts/timeouts.js'; +import type { ParsedRetryConfig} from '@/src/lib/api/retries/cfg-handler.js'; +import { RetryCfgHandler } from '@/src/lib/api/retries/cfg-handler.js'; +import { RetryPolicy } from '@/src/lib/api/retries/policy.js'; +import { NonErrorError } from '@/src/lib/errors.js'; +import type { SomeDoc } from '@/src/documents/index.js'; +import { DataAPIResponseError } from '@/src/documents/index.js'; +import type { BaseRequestMetadata } from '@/src/lib/api/clients/index.js'; +import type { ExplicitRetryConfig } from '@/src/lib/api/retries/config.js'; + +/** + * @internal + */ +export interface RetryAdapter { + policy: Exclude, + mkEphemeralCtx(ctx: InternalRetryContext, duration: number, error: Error, req: ReqMeta): Ctx, + emitRetryEvent(ctx: Ctx, meta: ReqMeta): void, + emitRetryDeclinedEvent(ctx: Ctx, meta: ReqMeta): void, + TimeoutError: SomeConstructor, +} + +/** + * @internal + */ +export abstract class RetryManager { + public static mk(isSafelyRetryable: boolean, opts: CommandOptions, adapter: RetryAdapter, basePolicy: ParsedRetryConfig): RetryManager { + if (opts.retry ?? basePolicy) { + const policies = RetryCfgHandler.concatParseWithin([basePolicy], opts, 'retry'); + const policy = policies?.[adapter.policy] ?? policies?.defaultPolicy; + + if (policy && !(policy as unknown instanceof RetryPolicy.Never)) { + return new RetryingImpl(policy, opts.isSafelyRetryable ?? isSafelyRetryable, adapter); + } + } + + return PassthroughImpl; + } + + public abstract run(meta: ReqMeta, tm: TimeoutManager, fn: () => Promise): Promise; +} + +/** + * @internal + */ +class RetryingImpl extends RetryManager { + private readonly _adapter: RetryAdapter; + + private readonly _policy: RetryPolicy; + + private readonly _isSafelyRetryable: boolean; + + private readonly _retryDurationTracker = new RetryDurationTracker(); + + constructor(policy: RetryPolicy, isSafelyRetryable: boolean, adapter: RetryAdapter) { + super(); + this._policy = policy; + this._isSafelyRetryable = isSafelyRetryable; + this._adapter = adapter; + } + + public override async run(metadata: ReqMeta, tm: TimeoutManager, fn: () => Promise) { + const baseCtx = new InternalRetryContext(this._isSafelyRetryable, metadata.timeout, metadata.requestId); + + while (true) { + const ephemeralDurationTracker = this._retryDurationTracker.forRequest(); + + try { + return await fn(); + } catch (caught) { + const ephemeralCtx = this._mkEphemeralCtx(caught, baseCtx, metadata); + + if (!this._shouldRetry(ephemeralCtx)) { + this._emitRetryDeclined(ephemeralCtx, metadata); + throw ephemeralCtx.error; + } + + baseCtx.retryCount++; + this._emitRetryAccepted(ephemeralCtx, metadata); + + const delay = this._policy.retryDelay(ephemeralCtx); + + if (delay) { + await new Promise(resolve => setTimeout(resolve, delay)); + } + + if (this._policy.shouldResetTimeout(ephemeralCtx)) { + ephemeralDurationTracker.updateRetryDebt(); + } + } finally { + tm.retard(ephemeralDurationTracker.endAndConsumeDebt()); + } + } + } + + private _mkEphemeralCtx(error: unknown, baseCtx: InternalRetryContext, metadata: ReqMeta) { + return this._adapter.mkEphemeralCtx(baseCtx, performance.now() - metadata.startTime, NonErrorError.asError(error), metadata); + } + + private _shouldRetry(ephemeralCtx: Ctx) { + return this._passesInitialRetryChecks(ephemeralCtx) && this._policy.shouldRetry(ephemeralCtx); + } + + private _passesInitialRetryChecks(ephemeralCtx: Ctx) { + if (ephemeralCtx.retryCount >= this._policy.maxRetries(ephemeralCtx)) { + return false; + } + + if (ephemeralCtx.error instanceof this._adapter.TimeoutError) { + return false; + } + + if (ephemeralCtx.isSafelyRetryable) { + return !(ephemeralCtx.error instanceof DataAPIResponseError) || (ephemeralCtx.error as SomeDoc).canRetry === true; // TODO: Swap for actual Data API implementation once available + } else { + return ephemeralCtx.error instanceof DataAPIResponseError && (ephemeralCtx.error as SomeDoc).canRetry === true; + } + } + + private _emitRetryAccepted(ephemeralCtx: Ctx, metadata: ReqMeta) { + this._policy.onRetry(ephemeralCtx); + this._adapter.emitRetryEvent(ephemeralCtx, metadata); + } + + private _emitRetryDeclined(ephemeralCtx: Ctx, metadata: ReqMeta) { + this._policy.onRetryDeclined(ephemeralCtx); + this._adapter.emitRetryDeclinedEvent(ephemeralCtx, metadata); + } +} + +class RetryDurationTracker { + private _runningCount = 0; + private _debtLastUpdated?: number; + private _debt = 0; + + public forRequest() { + this._runningCount++; + + if (this._debtLastUpdated === undefined) { + this._debtLastUpdated = performance.now(); + } + + return { + updateRetryDebt: () => { + this._updateDebt(this._debt + (performance.now() - this._debtLastUpdated!)); + }, + endAndConsumeDebt: () => { + this._runningCount--; + const debt = this._debt; + this._updateDebt(0); + return debt; + }, + }; + } + + private _updateDebt(debt: number) { + this._debt = debt; + + if (this._runningCount) { + this._debtLastUpdated = performance.now(); + } else { + this._debtLastUpdated = undefined; + } + } +} + +/** + * @internal + */ +const PassthroughImpl = new class PassthroughImpl extends RetryManager { + public override run(_: never, __: never, fn: () => Promise) { + return fn(); + } +}(); diff --git a/src/lib/api/retries/policy.ts b/src/lib/api/retries/policy.ts new file mode 100644 index 00000000..6dec9cf9 --- /dev/null +++ b/src/lib/api/retries/policy.ts @@ -0,0 +1,60 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { asMut } from '@/src/lib/utils.js'; +import type { RetryContext } from '@/src/lib/api/retries/contexts/base.js'; + +export abstract class RetryPolicy { + public static readonly Default: typeof DefaultRetryPolicy; + public static readonly Never: typeof NeverRetryPolicy; + + public abstract maxRetries(ctx: Ctx): number; + + public abstract retryDelay(ctx: Ctx): number; + + public shouldRetry(_: Ctx): boolean { + return true; + } + + public shouldResetTimeout(_: Ctx): boolean { + return false; + } + + public onRetry(_: Ctx): void {} + + public onRetryDeclined(_: Ctx): void {} +} + +class DefaultRetryPolicy extends RetryPolicy { + public maxRetries(): number { + return 3; + } + + public retryDelay(): number { + return 0; + } +} + +class NeverRetryPolicy extends RetryPolicy { + public maxRetries(): number { + return 0; + } + + public retryDelay(): number { + return 0; + } +} + +asMut(RetryPolicy).Default = DefaultRetryPolicy; +asMut(RetryPolicy).Never = NeverRetryPolicy; diff --git a/src/lib/api/ser-des/ser-des.ts b/src/lib/api/ser-des/ser-des.ts index d942811a..3a5a5e92 100644 --- a/src/lib/api/ser-des/ser-des.ts +++ b/src/lib/api/ser-des/ser-des.ts @@ -135,7 +135,7 @@ export abstract class SerDes = any, DesCtx extend ) { [this._serializers, this._deserializers] = processCodecs(this._cfg.codecs.flat()); } - + public serialize(obj: unknown, target: SerDesTarget = SerDesTarget.Record): [unknown, boolean] { if (obj === null || obj === undefined) { return [obj, false]; diff --git a/src/lib/api/timeouts/timeouts.ts b/src/lib/api/timeouts/timeouts.ts index a1ebf6bb..21730d14 100644 --- a/src/lib/api/timeouts/timeouts.ts +++ b/src/lib/api/timeouts/timeouts.ts @@ -263,7 +263,9 @@ export interface WithTimeout { /** * @internal */ -export type MkTimeoutError = (info: HTTPRequestInfo, timeoutType: TimedOutCategories) => Error; +export interface TimeoutAdapter { + mkTimeoutError(info: HTTPRequestInfo, timeoutType: TimedOutCategories): Error, +} /** * @internal @@ -271,6 +273,7 @@ export type MkTimeoutError = (info: HTTPRequestInfo, timeoutType: TimedOutCatego export interface TimeoutManager { initial(): Partial, advance(info: HTTPRequestInfo): [number, () => Error], + retard(amount: number): void, } /** @@ -295,7 +298,7 @@ export class Timeouts { public readonly baseTimeouts: TimeoutDescriptor; - constructor(private readonly _mkTimeoutError: MkTimeoutError, baseTimeouts: ParsedTimeoutDescriptor) { + constructor(private readonly _adapter: TimeoutAdapter, baseTimeouts: ParsedTimeoutDescriptor) { this.baseTimeouts = TimeoutCfgHandler.concat([Timeouts.Default, baseTimeouts]) as TimeoutDescriptor; } @@ -308,8 +311,26 @@ export class Timeouts { [key]: timeout, }; + let started: number; + let retardAmount = 0; + return this.custom(initial, () => { - return [timeout, 'provided']; + if (!started) { + started = Date.now(); + } + + const elapsed = Date.now() - started; + const adjustedOverallTimeout = timeout + retardAmount; + const overallLeft = adjustedOverallTimeout - elapsed; + const effectiveTimeout = Math.min(timeout, overallLeft); // Request timeout stays same, but can't exceed overall left + + if (effectiveTimeout <= 0) { + return [0, 'provided']; + } + + return [effectiveTimeout, 'provided']; + }, (amount) => { + retardAmount += amount; }); } @@ -318,17 +339,35 @@ export class Timeouts { [key]: (override?.timeout?.[key] ?? this.baseTimeouts[key]) || EffectivelyInfinity, }; - const timeout = Math.min(timeouts.requestTimeoutMs, timeouts[key]); - - const type: TimedOutCategories = - (timeouts.requestTimeoutMs === timeouts[key]) - ? ['requestTimeoutMs', key] : - (timeouts.requestTimeoutMs < timeouts[key]) - ? ['requestTimeoutMs'] - : [key]; + let started: number; + let retardAmount = 0; return this.custom(timeouts, () => { - return [timeout, type]; + if (!started) { + started = performance.now(); + } + + const elapsed = performance.now() - started; + const adjustedOverallTimeout = timeouts[key] + retardAmount; + const overallLeft = adjustedOverallTimeout - elapsed; + + // Request timeout stays constant, but check against remaining overall time + const effectiveRequestTimeout = Math.min(timeouts.requestTimeoutMs, overallLeft); + + if (overallLeft <= 0) { + return [0, [key]]; + } + + const type: TimedOutCategories = + (effectiveRequestTimeout === overallLeft) + ? ['requestTimeoutMs', key] : + (effectiveRequestTimeout < overallLeft) + ? ['requestTimeoutMs'] + : [key]; + + return [effectiveRequestTimeout, type]; + }, (amount) => { + retardAmount += amount; }); } @@ -354,13 +393,16 @@ export class Timeouts { }; let started: number; + let retardAmount = 0; return this.custom(initial, () => { if (!started) { - started = Date.now(); + started = performance.now(); } - const overallLeft = overallTimeout - (Date.now() - started); + const elapsed = performance.now() - started; + const adjustedOverallTimeout = overallTimeout + retardAmount; + const overallLeft = adjustedOverallTimeout - elapsed; if (overallLeft < requestTimeout) { return [overallLeft, [key]]; @@ -369,10 +411,12 @@ export class Timeouts { } else { return [overallLeft, ['requestTimeoutMs', key]]; } + }, (amount) => { + retardAmount += amount; }); } - public custom(peek: Partial, advance: () => [number, TimedOutCategories]): TimeoutManager { + public custom(peek: Partial, advance: () => [number, TimedOutCategories], retard?: (amount: number) => void): TimeoutManager { return { initial() { return peek; @@ -380,9 +424,12 @@ export class Timeouts { advance: (info) => { const advanced = advance() as any; const timeoutType = advanced[1]; - advanced[1] = () => this._mkTimeoutError(info, timeoutType); + advanced[1] = () => this._adapter.mkTimeoutError(info, timeoutType); return advanced; }, + retard(amount: number) { + retard?.(amount); + }, }; } diff --git a/src/lib/logging/base-event.ts b/src/lib/logging/base-event.ts index 75b59e07..befc1bf2 100644 --- a/src/lib/logging/base-event.ts +++ b/src/lib/logging/base-event.ts @@ -14,6 +14,7 @@ import type { DataAPIClientEvent } from '@/src/lib/index.js'; import type { SomeDoc } from '@/src/documents/index.js'; +import type { RequestId } from '@/src/lib/api/clients/utils/request-id.js'; /** * @internal @@ -213,9 +214,9 @@ export abstract class BaseClientEvent { * * @internal */ - protected constructor(name: string, requestId: string, extra: Record | undefined) { + protected constructor(name: string, requestId: RequestId, extra: Record | undefined) { this.name = name; - this.requestId = requestId; + this.requestId = requestId.unwrap; this.extraLogInfo = (extra && Object.keys(extra).length > 0) ? extra : undefined; this.timestamp = new Date(); diff --git a/src/lib/types.ts b/src/lib/types.ts index d69e9b7a..fbcc48ce 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -15,6 +15,7 @@ import type { DataAPIEnvironments } from '@/src/lib/constants.js'; import type { TimeoutDescriptor } from '@/src/lib/api/index.js'; +import type { RetryConfig } from '@/src/lib/api/retries/config.js'; /** * Shorthand type to represent some nullish value. @@ -38,6 +39,13 @@ export type DataAPIEnvironment = typeof DataAPIEnvironments[number]; */ export interface Ref { ref: T } +/** + * @internal + */ +export type Mut = { + -readonly [K in keyof T]: T[K]; +} + /** * Utility type to represent an empty object without eslint complaining. * @@ -157,4 +165,6 @@ export interface CommandOptions(json: string, otherwise: T, reviver?: (this: unk } } +/** + * @internal + */ +export const asMut = (t: T): Mut => t; + /** * ##### Overview * diff --git a/tests/integration/administration/lifecycle.test.ts b/tests/integration/administration/lifecycle.test.ts index 37f3c08d..8ceea11e 100644 --- a/tests/integration/administration/lifecycle.test.ts +++ b/tests/integration/administration/lifecycle.test.ts @@ -18,6 +18,7 @@ import { DevOpsAPIResponseError } from '@/src/administration/index.js'; import { background, Cfg, initTestObjects, it } from '@/tests/testlib/index.js'; import { DEFAULT_KEYSPACE, HttpMethods } from '@/src/lib/api/constants.js'; import { buildAstraEndpoint } from '@/src/lib/utils.js'; +import { RequestId } from '@/src/lib/api/clients/utils/request-id.js'; background('(ADMIN) (LONG) (NOT-DEV) (ASTRA) integration.administration.lifecycle', () => { it('works', async () => { @@ -133,14 +134,18 @@ background('(ADMIN) (LONG) (NOT-DEV) (ASTRA) integration.administration.lifecycl } { - await asyncDbAdmin._httpClient._awaitStatus(asyncDb.id, {} as any, { + await asyncDbAdmin._httpClient._awaitStatus(asyncDb.id, { + timeoutManager: asyncDbAdmin._httpClient.tm.multipart('generalMethodTimeoutMs', { timeout: 0 }), + }, { target: 'ACTIVE', legalStates: ['PENDING', 'INITIALIZING'], defaultPollInterval: 10000, id: null!, options: undefined, - timeoutManager: asyncDbAdmin._httpClient.tm.multipart('generalMethodTimeoutMs', { timeout: 0 }), - }, 0); + }, { + requestId: new RequestId(), + reqOpts: {}, + }); } for (const [dbAdmin, db, dbType] of [[syncDbAdmin, syncDb, 'sync'], [asyncDbAdmin, asyncDb, 'async']] as const) { @@ -184,14 +189,18 @@ background('(ADMIN) (LONG) (NOT-DEV) (ASTRA) integration.administration.lifecycl { await syncDbAdmin.createKeyspace('other_keyspace'); - await asyncDbAdmin._httpClient._awaitStatus(asyncDb.id, {} as any, { + await asyncDbAdmin._httpClient._awaitStatus(asyncDb.id, { + timeoutManager: asyncDbAdmin._httpClient.tm.multipart('generalMethodTimeoutMs', { timeout: 0 }), + }, { target: 'ACTIVE', legalStates: ['MAINTENANCE'], - defaultPollInterval: 1000, + defaultPollInterval: 10000, id: null!, options: undefined, - timeoutManager: asyncDbAdmin._httpClient.tm.multipart('generalMethodTimeoutMs', { timeout: 0 }), - }, 0); + }, { + requestId: new RequestId(), + reqOpts: {}, + }); } for (const [dbAdmin, db, dbType] of [[syncDbAdmin, syncDb, 'sync'], [asyncDbAdmin, asyncDb, 'async']] as const) { @@ -209,14 +218,18 @@ background('(ADMIN) (LONG) (NOT-DEV) (ASTRA) integration.administration.lifecycl { await syncDbAdmin.dropKeyspace('other_keyspace', { blocking: true }); - await asyncDbAdmin._httpClient._awaitStatus(asyncDb.id, {} as any, { + await asyncDbAdmin._httpClient._awaitStatus(asyncDb.id, { + timeoutManager: asyncDbAdmin._httpClient.tm.multipart('generalMethodTimeoutMs', { timeout: 0 }), + }, { target: 'ACTIVE', legalStates: ['MAINTENANCE'], - defaultPollInterval: 1000, + defaultPollInterval: 10000, id: null!, options: undefined, - timeoutManager: asyncDbAdmin._httpClient.tm.multipart('generalMethodTimeoutMs', { timeout: 0 }), - }, 0); + }, { + requestId: new RequestId(), + reqOpts: {}, + }); } for (const [dbAdmin, db, dbType] of [[syncDbAdmin, syncDb, 'sync'], [asyncDbAdmin, asyncDb, 'async']] as const) { @@ -236,14 +249,18 @@ background('(ADMIN) (LONG) (NOT-DEV) (ASTRA) integration.administration.lifecycl { await admin.dropDatabase(syncDb, { timeout: 1440000 }); - await asyncDbAdmin._httpClient._awaitStatus(asyncDb.id, {} as any, { + await asyncDbAdmin._httpClient._awaitStatus(asyncDb.id, { + timeoutManager: asyncDbAdmin._httpClient.tm.multipart('generalMethodTimeoutMs', { timeout: 0 }), + }, { target: 'TERMINATED', legalStates: ['TERMINATING'], defaultPollInterval: 10000, id: null!, options: undefined, - timeoutManager: asyncDbAdmin._httpClient.tm.multipart('generalMethodTimeoutMs', { timeout: 0 }), - }, 0); + }, { + requestId: new RequestId(), + reqOpts: {}, + }); } for (const [dbAdmin, dbType] of [[syncDbAdmin, 'sync'], [asyncDbAdmin, 'async']] as const) { diff --git a/tests/integration/client/data-api-client.test.ts b/tests/integration/client/data-api-client.test.ts index 38d755fb..66e78e99 100644 --- a/tests/integration/client/data-api-client.test.ts +++ b/tests/integration/client/data-api-client.test.ts @@ -165,7 +165,7 @@ describe('integration.client.data-api-client', () => { assert.deepStrictEqual(stderr, []); }); - it('should allow monitoring of failed commands when enabled', async () => { + it('should allow monitoring of failed commands when enabled', async (key) => { const client = new DataAPIClient(Cfg.DbToken, { environment: Cfg.DbEnvironment }); const db = client.db(Cfg.DbUrl, { logging: ['all', { events: 'commandSucceeded', emits: ['event', 'stdout'] }], keyspace: DEFAULT_KEYSPACE }); const collection = db.collection(Cfg.DefaultCollectionName); @@ -173,7 +173,7 @@ describe('integration.client.data-api-client', () => { let startedEvent: CommandStartedEvent | undefined; let failedEvent: CommandFailedEvent | undefined; - await collection.insertOne({ _id: 0, name: 'Oasis' }); + await collection.insertOne({ _id: key, name: 'Oasis' }); stdout = []; stderr = []; @@ -189,12 +189,7 @@ describe('integration.client.data-api-client', () => { failedEvent = event; }); - try { - await collection.insertOne({ _id: 0, name: 'Oasis' }); - assert.fail('should have thrown an error'); - } catch (e) { - assert.ok(e instanceof Error); - } + await assert.rejects(() => collection.insertOne({ _id: key, name: 'Oasis' })); assert.ok(startedEvent instanceof CommandStartedEvent); assert.ok(failedEvent instanceof CommandFailedEvent); @@ -214,8 +209,8 @@ describe('integration.client.data-api-client', () => { assert.deepStrictEqual(startedEvent.timeout, { generalMethodTimeoutMs: Timeouts.Default.generalMethodTimeoutMs, requestTimeoutMs: Timeouts.Default.requestTimeoutMs }); assert.ok(failedEvent.duration > 0); - assert.deepStrictEqual(startedEvent.command, { insertOne: { document: { _id: 0, name: 'Oasis' } } }); - assert.deepStrictEqual(failedEvent.command, { insertOne: { document: { _id: 0, name: 'Oasis' } } }); + assert.deepStrictEqual(startedEvent.command, { insertOne: { document: { _id: key, name: 'Oasis' } } }); + assert.deepStrictEqual(failedEvent.command, { insertOne: { document: { _id: key, name: 'Oasis' } } }); assert.ok(failedEvent.error instanceof DataAPIResponseError); assert.strictEqual(failedEvent.error.errorDescriptors.length, 1); diff --git a/tests/integration/db/db.test.ts b/tests/integration/db/db.test.ts index ae2857b7..c25526db 100644 --- a/tests/integration/db/db.test.ts +++ b/tests/integration/db/db.test.ts @@ -220,7 +220,7 @@ parallel('integration.db.db', { drop: 'colls:after' }, ({ db }) => { it('should throw an error if no keyspace set', async () => { const { db } = initTestObjects(); db.useKeyspace(undefined!); - await assert.rejects(() => db.command({ findEmbeddingProviders: {} }), { message: 'Db is missing a required keyspace; be sure to set one with client.db(..., { keyspace }), or db.useKeyspace()' }); + await assert.rejects(() => db.command({ findEmbeddingProviders: {} }), { message: 'Db is missing a working keyspace; set one with client.db(..., { keyspace }) or db.useKeyspace()' }); }); it('should not throw an error if no keyspace set but keyspace: null', async () => { diff --git a/tests/integration/lib/api/clients/data-api-http-client.test.ts b/tests/integration/lib/api/clients/data-api-http-client.test.ts index 4b0d9eca..a34bb431 100644 --- a/tests/integration/lib/api/clients/data-api-http-client.test.ts +++ b/tests/integration/lib/api/clients/data-api-http-client.test.ts @@ -15,7 +15,7 @@ import { Cfg, describe, initTestObjects, it, parallel } from '@/tests/testlib/index.js'; import assert from 'assert'; -import type { DataAPIHttpClient } from '@/src/lib/api/clients/data-api-http-client.js'; +import type { DataAPIHttpClient } from '@/src/lib/api/clients/impls/data-api-http-client.js'; import { DataAPIHttpError, DataAPIResponseError } from '@/src/documents/index.js'; describe('integration.lib.api.clients.data-api-http-client', ({ db }) => { diff --git a/tests/unit/administration/admin.test.ts b/tests/unit/administration/admin.test.ts index 9b12f873..5e8db98d 100644 --- a/tests/unit/administration/admin.test.ts +++ b/tests/unit/administration/admin.test.ts @@ -15,7 +15,7 @@ import assert from 'assert'; import { AstraAdmin } from '@/src/administration/index.js'; import { StaticTokenProvider, TokenProvider } from '@/src/lib/index.js'; -import type { AdminOptions, DbOptions } from '@/src/client/index.js'; +import type { AdminOptions, DbOptions } from '@/src/client/index.js'; import { DataAPIClient } from '@/src/client/index.js'; import { describe, it } from '@/tests/testlib/index.js'; import { DEFAULT_DEVOPS_API_ENDPOINTS } from '@/src/lib/api/constants.js'; @@ -33,13 +33,13 @@ describe('unit.administration.admin', () => { it('should properly construct an AstraAdmin object', () => { const admin = new AstraAdmin(internalOps(), AdminOptsHandler.empty); assert.ok(admin); - assert.strictEqual(admin._httpClient.baseUrl, DEFAULT_DEVOPS_API_ENDPOINTS.prod); + assert.strictEqual(admin._httpClient._baseUrl, DEFAULT_DEVOPS_API_ENDPOINTS.prod); }); it('should properly construct an AstraAdmin object with a custom astra environment', () => { const admin = new AstraAdmin(internalOps({}, { astraEnv: 'dev' }), AdminOptsHandler.empty); assert.ok(admin); - assert.strictEqual(admin._httpClient.baseUrl, 'https://api.dev.cloud.datastax.com/v2'); + assert.strictEqual(admin._httpClient._baseUrl, 'https://api.dev.cloud.datastax.com/v2'); }); it('should not throw on missing token', () => { @@ -50,7 +50,7 @@ describe('unit.administration.admin', () => { it('should allow admin construction using default options', () => { const admin = new AstraAdmin(internalOps({}, { endpointUrl: 'https://api.astra.datastax.com/v1' }), AdminOptsHandler.empty); assert.ok(admin); - assert.strictEqual(admin._httpClient.baseUrl, 'https://api.astra.datastax.com/v1'); + assert.strictEqual(admin._httpClient._baseUrl, 'https://api.astra.datastax.com/v1'); }); it('should allow admin construction, overwriting options', () => { @@ -59,7 +59,7 @@ describe('unit.administration.admin', () => { astraEnv: 'dev', })); assert.ok(admin); - assert.strictEqual(admin._httpClient.baseUrl, 'https://api.dev.cloud.datastax.com/v2'); + assert.strictEqual(admin._httpClient._baseUrl, 'https://api.dev.cloud.datastax.com/v2'); }); }); diff --git a/tests/unit/client/data-api-client.test.ts b/tests/unit/client/data-api-client.test.ts index 27a36ecb..639be997 100644 --- a/tests/unit/client/data-api-client.test.ts +++ b/tests/unit/client/data-api-client.test.ts @@ -144,22 +144,22 @@ describe('unit.client.data-api-client', () => { it('uses http2 by default', function () { const client = new DataAPIClient('dummy-token', { httpOptions: { client: 'fetch-h2', fetchH2 } }); const httpClient = client.db(Cfg.DbUrl)._httpClient; - assert.ok(httpClient.fetchCtx.ctx instanceof FetchH2); - assert.ok(httpClient.fetchCtx.ctx._http1 !== httpClient.fetchCtx.ctx._preferred); + assert.ok(httpClient._fetchCtx.ctx instanceof FetchH2); + assert.ok(httpClient._fetchCtx.ctx._http1 !== httpClient._fetchCtx.ctx._preferred); }); it('uses http2 when forced', function () { const client = new DataAPIClient('dummy-token', { httpOptions: { client: 'fetch-h2', fetchH2, preferHttp2: true } }); const httpClient = client.db(Cfg.DbUrl)._httpClient; - assert.ok(httpClient.fetchCtx.ctx instanceof FetchH2); - assert.ok(httpClient.fetchCtx.ctx._http1 !== httpClient.fetchCtx.ctx._preferred); + assert.ok(httpClient._fetchCtx.ctx instanceof FetchH2); + assert.ok(httpClient._fetchCtx.ctx._http1 !== httpClient._fetchCtx.ctx._preferred); }); it('uses http1.1 when forced', () => { const client = new DataAPIClient('dummy-token', { httpOptions: { client: 'fetch-h2', fetchH2, preferHttp2: false } }); const httpClient = client.db(Cfg.DbUrl)._httpClient; - assert.ok(httpClient.fetchCtx.ctx instanceof FetchH2); - assert.ok(httpClient.fetchCtx.ctx._http1 === httpClient.fetchCtx.ctx._preferred); + assert.ok(httpClient._fetchCtx.ctx instanceof FetchH2); + assert.ok(httpClient._fetchCtx.ctx._http1 === httpClient._fetchCtx.ctx._preferred); }); }); @@ -176,8 +176,8 @@ describe('unit.client.data-api-client', () => { }); const httpClient = client.db(Cfg.DbUrl)._httpClient; - assert.strictEqual(await httpClient.fetchCtx.ctx.fetch(null!), 3); - assert.strictEqual(httpClient.fetchCtx.ctx.close, undefined); + assert.strictEqual(await httpClient._fetchCtx.ctx.fetch(null!), 3); + assert.strictEqual(httpClient._fetchCtx.ctx.close, undefined); }); it('should throw if fetcher not properly implemented', () => { diff --git a/tests/unit/db/db.test.ts b/tests/unit/db/db.test.ts index 7d5d3b61..a1c5d034 100644 --- a/tests/unit/db/db.test.ts +++ b/tests/unit/db/db.test.ts @@ -36,13 +36,13 @@ describe('unit.db.db', () => { it('should allow db construction from endpoint', () => { const db = new Db(internalOps(), 'https://id-region.apps.astra.datastax.com', DbOptsHandler.empty); assert.ok(db); - assert.strictEqual(db._httpClient.baseUrl, `https://id-region.apps.astra.datastax.com/${DEFAULT_DATA_API_PATHS.astra}`); + assert.strictEqual(db._httpClient._baseUrl, `https://id-region.apps.astra.datastax.com/${DEFAULT_DATA_API_PATHS.astra}`); }); it('should trim trailing slash in endpoints', () => { for (let i = 0; i < 10; i++) { const db = new Db(internalOps(), `https://id-region.apps.astra.datastax.com${'/'.repeat(i)}`, DbOptsHandler.empty); - assert.strictEqual(db._httpClient.baseUrl, `https://id-region.apps.astra.datastax.com/${DEFAULT_DATA_API_PATHS.astra}`); + assert.strictEqual(db._httpClient._baseUrl, `https://id-region.apps.astra.datastax.com/${DEFAULT_DATA_API_PATHS.astra}`); } }); @@ -56,13 +56,13 @@ describe('unit.db.db', () => { it('should allow db construction from endpoint, using default options', () => { const db = new Db(internalOps(), 'https://id-region.apps.astra.datastax.com', DbOptsHandler.empty); assert.ok(db); - assert.strictEqual(db._httpClient.baseUrl, `https://id-region.apps.astra.datastax.com/${DEFAULT_DATA_API_PATHS.astra}`); + assert.strictEqual(db._httpClient._baseUrl, `https://id-region.apps.astra.datastax.com/${DEFAULT_DATA_API_PATHS.astra}`); }); it('should allow db construction from endpoint, overwriting options', () => { const db = new Db(internalOps({ dataApiPath: 'old', keyspace: 'old' }), 'https://id-region.apps.astra.datastax.com', DbOptsHandler.parse({ dataApiPath: 'new', keyspace: 'new' })); assert.ok(db); - assert.strictEqual(db._httpClient.baseUrl, 'https://id-region.apps.astra.datastax.com/new'); + assert.strictEqual(db._httpClient._baseUrl, 'https://id-region.apps.astra.datastax.com/new'); assert.strictEqual(db.keyspace, 'new'); }); @@ -95,12 +95,12 @@ describe('unit.db.db', () => { it('handles different dataApiPath', () => { const db = new Db(internalOps({ dataApiPath: 'api/json/v2' }), Cfg.DbUrl, DbOptsHandler.empty); - assert.strictEqual(db._httpClient.baseUrl, `${Cfg.DbUrl}/api/json/v2`); + assert.strictEqual(db._httpClient._baseUrl, `${Cfg.DbUrl}/api/json/v2`); }); it('handles different dataApiPath when overridden', () => { const db = new Db(internalOps({ dataApiPath: 'api/json/v2' }), Cfg.DbUrl, DbOptsHandler.parse({ dataApiPath: 'api/json/v3' })); - assert.strictEqual(db._httpClient.baseUrl, `${Cfg.DbUrl}/api/json/v3`); + assert.strictEqual(db._httpClient._baseUrl, `${Cfg.DbUrl}/api/json/v3`); }); it('should accept valid logging', () => { diff --git a/tests/unit/lib/api/clients/headers-resolver.test.ts b/tests/unit/lib/api/clients/headers-resolver.test.ts index bf89c219..cfe7f470 100644 --- a/tests/unit/lib/api/clients/headers-resolver.test.ts +++ b/tests/unit/lib/api/clients/headers-resolver.test.ts @@ -14,7 +14,7 @@ import assert from 'assert'; import { describe, it } from '@/tests/testlib/index.js'; -import { HeadersResolver } from '@/src/lib/api/clients/headers-resolver.js'; +import { HeadersResolver } from '@/src/lib/api/clients/utils/headers-resolver.js'; import { EmbeddingAPIKeyHeaderProvider, HeadersProvider, TokenProvider } from '@/src/lib/index.js'; import { DEFAULT_DATA_API_AUTH_HEADER } from '@/src/lib/api/constants.js'; import fc from 'fast-check'; @@ -41,7 +41,7 @@ describe('unit.lib.api.clients.headers-resolver', () => { TokenProvider.opts.parse('new').toHeadersProvider(), ]); - const hr = new HeadersResolver('data-api', providers, { + const hr = new HeadersResolver({ target: 'data-api' }, providers, { 'x-foo': 'bar', 'car': 'bus', }); @@ -69,7 +69,7 @@ describe('unit.lib.api.clients.headers-resolver', () => { rawHeaders.map((h) => HeadersProvider.opts.fromObj.parse(h)), ); - const hr = new HeadersResolver('data-api', providers, baseHeaders); + const hr = new HeadersResolver({ target: 'data-api' }, providers, baseHeaders); const headers = hr.resolve(); const expected = mergeObjsIgnoringUndefined(baseHeaders, ...rawHeaders); @@ -105,7 +105,7 @@ describe('unit.lib.api.clients.headers-resolver', () => { TokenProvider.opts.parse('new').toHeadersProvider(), ]); - const hr = new HeadersResolver('data-api', providers, { + const hr = new HeadersResolver({ target: 'data-api' }, providers, { 'x-foo': 'bar', 'car': 'bus', }); @@ -146,7 +146,7 @@ describe('unit.lib.api.clients.headers-resolver', () => { }), ); - const hr = new HeadersResolver('data-api', providers, baseHeaders); + const hr = new HeadersResolver({ target: 'data-api' }, providers, baseHeaders); const headers = hr.resolve(); const expected = mergeObjsIgnoringUndefined(baseHeaders, ...rawHeaders); diff --git a/tests/unit/lib/api/timeouts.test.ts b/tests/unit/lib/api/timeouts.test.ts index a2d94cde..ea589562 100644 --- a/tests/unit/lib/api/timeouts.test.ts +++ b/tests/unit/lib/api/timeouts.test.ts @@ -28,7 +28,7 @@ describe('unit.lib.api.timeouts', () => { } } - const timeouts = new Timeouts((info, timeoutType) => new TimeoutError(info, timeoutType), Timeouts.Default); + const timeouts = new Timeouts({ mkTimeoutError: (info, timeoutType) => new TimeoutError(info, timeoutType) }, Timeouts.Default); const info = (timeoutManager: TimeoutManager) => ({ timeoutManager }) as HTTPRequestInfo; describe('single', () => {