diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 0b836e119..d9b4b9749 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,4 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:e6d785d6de3cab027f6213d95ccedab4cab3811b0d3172b78db2216faa182e32 + digest: sha256:606f3d9d99a1c7cdfa7158cbb1a75bfeef490655e246a2052f9ee741740d736c +# created: 2023-08-17T19:15:55.176034173Z diff --git a/.kokoro/presubmit/node14/common.cfg b/.kokoro/presubmit/node14/common.cfg new file mode 100644 index 000000000..bda58c77b --- /dev/null +++ b/.kokoro/presubmit/node14/common.cfg @@ -0,0 +1,24 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "gax-nodejs/.kokoro/trampoline_v2.sh" + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" +} +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/gax-nodejs/.kokoro/test.sh" +} diff --git a/.kokoro/presubmit/node14/samples-test.cfg b/.kokoro/presubmit/node14/samples-test.cfg new file mode 100644 index 000000000..3fb9c46ca --- /dev/null +++ b/.kokoro/presubmit/node14/samples-test.cfg @@ -0,0 +1,12 @@ +# Download resources for system tests (service account key, etc.) +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/gax-nodejs/.kokoro/samples-test.sh" +} + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "long-door-651-kokoro-system-test-service-account" +} \ No newline at end of file diff --git a/.kokoro/presubmit/node14/system-test.cfg b/.kokoro/presubmit/node14/system-test.cfg new file mode 100644 index 000000000..8a980142a --- /dev/null +++ b/.kokoro/presubmit/node14/system-test.cfg @@ -0,0 +1,12 @@ +# Download resources for system tests (service account key, etc.) +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-nodejs" + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/gax-nodejs/.kokoro/system-test.sh" +} + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "long-door-651-kokoro-system-test-service-account" +} \ No newline at end of file diff --git a/.kokoro/presubmit/node14/test.cfg b/.kokoro/presubmit/node14/test.cfg new file mode 100644 index 000000000..e69de29bb diff --git a/.kokoro/release/docs-devsite.sh b/.kokoro/release/docs-devsite.sh index 2198e67fe..3596c1e4c 100755 --- a/.kokoro/release/docs-devsite.sh +++ b/.kokoro/release/docs-devsite.sh @@ -25,5 +25,6 @@ if [[ -z "$CREDENTIALS" ]]; then fi npm install -npm install --no-save @google-cloud/cloud-rad@^0.2.5 -npx @google-cloud/cloud-rad \ No newline at end of file +npm install --no-save @google-cloud/cloud-rad@^0.3.7 +# publish docs to devsite +npx @google-cloud/cloud-rad . cloud-rad diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a6e87677..00e2523e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,33 @@ [1]: https://www.npmjs.com/package/gax-nodejs?activeTab=versions +## [4.0.3](https://github.com/googleapis/gax-nodejs/compare/v4.0.2...v4.0.3) (2023-07-27) + + +### Bug Fixes + +* Make gapic-tools depend on gax-nodejs ([#1480](https://github.com/googleapis/gax-nodejs/issues/1480)) ([d0f410d](https://github.com/googleapis/gax-nodejs/commit/d0f410d2e08f393f2661c8c92568a0b518fddf99)) + +## [4.0.2](https://github.com/googleapis/gax-nodejs/compare/v4.0.1...v4.0.2) (2023-07-25) + + +### Bug Fixes + +* Update some pnpm deps ([#1478](https://github.com/googleapis/gax-nodejs/issues/1478)) ([39583d5](https://github.com/googleapis/gax-nodejs/commit/39583d5f4faab89b511fe317bd1ec3334c2ea3f5)) + +## [4.0.1](https://github.com/googleapis/gax-nodejs/compare/v4.0.0...v4.0.1) (2023-07-21) + + +### Bug Fixes + +* `rimraf` version + remove conflicting types ([#1475](https://github.com/googleapis/gax-nodejs/issues/1475)) ([f9f8b13](https://github.com/googleapis/gax-nodejs/commit/f9f8b1328c38718bf621b92338b3d81297525aa6)) +* Add missing devDependency for compodoc ([#1470](https://github.com/googleapis/gax-nodejs/issues/1470)) ([115e317](https://github.com/googleapis/gax-nodejs/commit/115e317728c8ae6fa0e61f54d0087e26382d8230)) +* **deps:** Update dependency google-auth-library to v9 ([#1476](https://github.com/googleapis/gax-nodejs/issues/1476)) ([8afdd59](https://github.com/googleapis/gax-nodejs/commit/8afdd591646a190fde38728f0df14c604643f5cc)) +* **deps:** Update dependency retry-request to v6 ([#1477](https://github.com/googleapis/gax-nodejs/issues/1477)) ([6401a88](https://github.com/googleapis/gax-nodejs/commit/6401a88c50fa0eb3eb8a73cefc830896369c3330)) +* **deps:** Update protobufjs ([#1467](https://github.com/googleapis/gax-nodejs/issues/1467)) ([0a7dd94](https://github.com/googleapis/gax-nodejs/commit/0a7dd948573bd9553a0e9548e9ab92dbcfcb7414)) +* Replace proto-over-HTTP with REGAPIC ([#1471](https://github.com/googleapis/gax-nodejs/issues/1471)) ([4266f43](https://github.com/googleapis/gax-nodejs/commit/4266f43922d0d582b8eced11f4a21c98a8b451fe)) +* The return types for IAM service methods should be arrays, to match ReturnTuple ([#1001](https://github.com/googleapis/gax-nodejs/issues/1001)) ([48eed95](https://github.com/googleapis/gax-nodejs/commit/48eed955e7329f55f9427a7bc0656cfe2af395e8)) + ## [4.0.0](https://github.com/googleapis/gax-nodejs/compare/v3.6.0...v4.0.0) (2023-05-17) diff --git a/client-libraries.md b/client-libraries.md index 47d115672..d64cea055 100644 --- a/client-libraries.md +++ b/client-libraries.md @@ -99,31 +99,20 @@ binary tranport based on HTTP/2. It's Node.js implementation, #### HTTP/1.1 REST API mode -- `options.fallback`: `true`, `"rest"`, or `false`, use HTTP fallback mode. +- `options.fallback`: `true` or `false`, use HTTP fallback mode. Default value is `false`, unless the `window` object is defined. + For compatibility, you can pass any non-empty string, it will be considered + a `true` value. If you need to use the client library in non-Node.js environment or when gRPC cannot be used for any reason, you can use the HTTP/1.1 fallback mode. In this mode, a special browser-compatible transport implementation is used instead of -gRPC transport. - -There are two supported gRPC fallback modes: - -- set `options.fallback` to `"rest"`: the library will send and receive JSON - payload, HTTP/1.1 REST API endpoints will be used. This mode is recommended - for fallback. - -- set `options.fallback` to `true`: the library will send and receive serialized - protobuf payload to special endpoints accepting serialized protobufs over - HTTP/1.1. +gRPC transport. It will send and receive JSONs over HTTP. In browser context (if the `window` object is defined) the fallback mode is enabled automatically; set `options.fallback` to `false` if you need to override this behavior. -Note that `options.fallback` accepts boolean values (`true` and `false`) for -compatibility only. We recommend using `"rest"` to use HTTP/1.1 instead of gRPC. - ## Calling API methods In all examples below we assume that `client` is an instance of the client diff --git a/package.json b/package.json index f2e67a299..231ee212b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-gax", - "version": "4.0.0", + "version": "4.0.3", "description": "Google API Extensions", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -10,50 +10,50 @@ "!build/src/**/*.map" ], "dependencies": { - "@grpc/grpc-js": "~1.8.0", + "@grpc/grpc-js": "~1.9.0", "@grpc/proto-loader": "^0.7.0", "@types/long": "^4.0.0", "abort-controller": "^3.0.0", "duplexify": "^4.0.0", - "google-auth-library": "^8.0.2", + "extend": "^3.0.2", + "google-auth-library": "^9.0.0", "node-fetch": "^2.6.1", "object-hash": "^3.0.0", - "proto3-json-serializer": "^1.0.0", - "protobufjs": "7.2.3", - "retry-request": "^5.0.0" + "proto3-json-serializer": "^2.0.0", + "protobufjs": "7.2.5", + "retry-request": "^6.0.0" }, "devDependencies": { - "@compodoc/compodoc": "1.1.19", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@compodoc/compodoc": "1.1.21", "@types/mocha": "^9.0.0", "@types/ncp": "^2.0.1", - "@types/node": "^18.0.0", + "@types/node": "^20.5.0", "@types/node-fetch": "^2.5.4", "@types/object-hash": "^3.0.0", "@types/proxyquire": "^1.3.28", "@types/pumpify": "^1.4.1", - "@types/rimraf": "^3.0.2", "@types/sinon": "^10.0.0", "@types/uglify-js": "^3.17.0", - "c8": "^7.0.0", + "c8": "^8.0.0", "codecov": "^3.1.0", "execa": "^5.0.0", - "gapic-tools": "^0.1.7", - "google-proto-files": "^3.0.0", - "gts": "^3.1.0", + "google-proto-files": "^4.0.0", + "gts": "^5.0.0", "linkinator": "^4.0.0", "long": "^4.0.0", "mkdirp": "^2.0.0", "mocha": "^9.0.0", "ncp": "^2.0.0", "null-loader": "^4.0.0", - "protobufjs-cli": "1.1.1", + "protobufjs-cli": "1.1.2", "proxyquire": "^2.0.1", "pumpify": "^2.0.0", - "rimraf": "^3.0.2", + "rimraf": "^5.0.1", "sinon": "^15.0.0", "stream-events": "^1.0.4", "ts-loader": "^8.0.0", - "typescript": "^4.6.4", + "typescript": "^5.1.6", "uglify-js": "^3.17.0", "walkdir": "^0.4.0", "webpack": "^4.0.0", @@ -74,7 +74,7 @@ "compile-http-protos": "pbjs -t static-module -r http_proto --keep-case google/api/http.proto -p ./protos > protos/http.js && pbts protos/http.js -o protos/http.d.ts", "compile-showcase-proto": "pbjs -t json google/showcase/v1beta1/echo.proto google/showcase/v1beta1/identity.proto google/showcase/v1beta1/messaging.proto google/showcase/v1beta1/testing.proto -p ./protos > test/fixtures/google-gax-packaging-test-app/protos/protos.json && pbjs -t static-module -r showcase_protos google/showcase/v1beta1/echo.proto google/showcase/v1beta1/identity.proto google/showcase/v1beta1/messaging.proto google/showcase/v1beta1/testing.proto -p ./protos > test/fixtures/google-gax-packaging-test-app/protos/protos.js && pbts test/fixtures/google-gax-packaging-test-app/protos/protos.js -o test/fixtures/google-gax-packaging-test-app/protos/protos.d.ts", "fix": "gts fix", - "prepare": "npm run compile && prepublishProtos . && mkdirp build/protos && cp -r protos/* build/protos/ && npm run minify-proto-json", + "prepare": "npm run compile && node ./build/tools/src/prepublish.js . && mkdirp build/protos && cp -r protos/* build/protos/ && npm run minify-proto-json", "system-test": "c8 mocha build/test/system-test --timeout 600000 && npm run test-application", "samples-test": "cd samples/ && npm link ../ && npm test && cd ../", "docs-test": "linkinator docs", @@ -83,8 +83,8 @@ "test-application": "cd test/test-application && npm run prefetch && npm install && npm start", "prelint": "cd samples; npm link ../; npm install", "precompile": "gts clean", - "update-protos": "listProtos .", - "minify-proto-json": "minifyProtoJson" + "update-protos": "node ./build/tools/src/listProtos.js .", + "minify-proto-json": "node ./build/tools/src/minify.js" }, "repository": "googleapis/gax-nodejs", "keywords": [ diff --git a/release-please-config.json b/release-please-config.json index a6ee711d8..96b781df8 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,9 +1,8 @@ { - "initial-version": "0.1.0", - "packages": { - ".": {}, - "tools": {} - }, - "release-type": "node" - } - \ No newline at end of file + "initial-version": "0.1.0", + "packages": { + ".": {}, + "tools": {} + }, + "release-type": "node" +} \ No newline at end of file diff --git a/samples/package.json b/samples/package.json index 1f790787a..f73facf9b 100644 --- a/samples/package.json +++ b/samples/package.json @@ -14,10 +14,10 @@ "*.js" ], "dependencies": { - "google-gax": "^4.0.0" + "google-gax": "^4.0.3" }, "devDependencies": { - "c8": "^7.0.0", + "c8": "^8.0.0", "mocha": "^9.0.0" } } diff --git a/src/clientInterface.ts b/src/clientInterface.ts index 4eb54cf9a..46d280002 100644 --- a/src/clientInterface.ts +++ b/src/clientInterface.ts @@ -37,6 +37,7 @@ export interface ClientOptions clientConfig?: gax.ClientConfig; fallback?: boolean | 'rest' | 'proto'; apiEndpoint?: string; + newRetry?: boolean; } export interface Descriptors { diff --git a/src/createApiCall.ts b/src/createApiCall.ts index 51a40bd3b..1acd12fe1 100644 --- a/src/createApiCall.ts +++ b/src/createApiCall.ts @@ -28,7 +28,7 @@ import { SimpleCallbackFunction, } from './apitypes'; import {Descriptor} from './descriptor'; -import {CallOptions, CallSettings} from './gax'; +import {CallOptions, CallSettings, checkRetryOptions} from './gax'; import {retryable} from './normalCalls/retries'; import {addTimeoutArg} from './normalCalls/timeout'; import {StreamingApiCaller} from './streamingCalls/streamingApiCaller'; @@ -63,7 +63,6 @@ export function createApiCall( // function. Currently client librares are only calling this method with a // promise, but it will change. const funcPromise = typeof func === 'function' ? Promise.resolve(func) : func; - // the following apiCaller will be used for all calls of this function... const apiCaller = createAPICaller(settings, descriptor); @@ -72,9 +71,21 @@ export function createApiCall( callOptions?: CallOptions, callback?: APICallback ) => { - const thisSettings = settings.merge(callOptions); - let currentApiCaller = apiCaller; + const gaxStreamingRetries = (currentApiCaller as StreamingApiCaller) + .descriptor?.gaxStreamingRetries; + let thisSettings: CallSettings; + if (currentApiCaller instanceof StreamingApiCaller) { + // If Gax streaming retries are enabled, check settings passed at call time and convert parameters if needed + const thisSettingsTemp = checkRetryOptions( + callOptions, + gaxStreamingRetries + ); + thisSettings = settings.merge(thisSettingsTemp); + } else { + thisSettings = settings.merge(callOptions); + } + // special case: if bundling is disabled for this one call, // use default API caller instead if (settings.isBundling && !thisSettings.isBundling) { @@ -89,23 +100,38 @@ export function createApiCall( const streaming = (currentApiCaller as StreamingApiCaller).descriptor ?.streaming; + const retry = thisSettings.retry; - if ( - !streaming && - retry && - retry.retryCodes && - retry.retryCodes.length > 0 - ) { - retry.backoffSettings.initialRpcTimeoutMillis = - retry.backoffSettings.initialRpcTimeoutMillis || - thisSettings.timeout; - return retryable( - func, - thisSettings.retry!, - thisSettings.otherArgs as GRPCCallOtherArgs, - thisSettings.apiName + if (!streaming && retry && retry.getResumptionRequestFn) { + throw new Error( + 'Resumption strategy can only be used with server streaming retries' ); } + if (!streaming && retry && retry.retryCodesOrShouldRetryFn) { + if ( + retry.retryCodesOrShouldRetryFn instanceof Array && + retry.retryCodesOrShouldRetryFn.length > 0 + ) { + retry.backoffSettings.initialRpcTimeoutMillis = + retry.backoffSettings.initialRpcTimeoutMillis || + thisSettings.timeout; + return retryable( + func, + thisSettings.retry!, + thisSettings.otherArgs as GRPCCallOtherArgs, + thisSettings.apiName + ); + } else { + if ( + retry.retryCodesOrShouldRetryFn instanceof Function && + !streaming + ) { + throw new Error( + 'Using a function to determine retry eligibility is only supported with server streaming calls' + ); + } + } + } return addTimeoutArg( func, thisSettings.timeout, diff --git a/src/fallback.ts b/src/fallback.ts index 0f3046e66..cbc49dbff 100644 --- a/src/fallback.ts +++ b/src/fallback.ts @@ -35,7 +35,6 @@ import {GaxCall, GRPCCall} from './apitypes'; import {Descriptor, StreamDescriptor} from './descriptor'; import {createApiCall as _createApiCall} from './createApiCall'; import {FallbackServiceError} from './googleError'; -import * as fallbackProto from './fallbackProto'; import * as fallbackRest from './fallbackRest'; import {isNodeJS} from './featureDetection'; import {generateServiceStub} from './fallbackServiceStub'; @@ -94,7 +93,7 @@ export type AuthClient = export class GrpcClient { auth?: OAuth2Client | GoogleAuth; authClient?: AuthClient; - fallback: boolean | 'rest' | 'proto'; + fallback: boolean; grpcVersion: string; private static protoCache = new Map(); httpRules?: Array; @@ -119,7 +118,11 @@ export class GrpcClient { constructor( options: (GrpcClientOptions | {auth: OAuth2Client}) & { - fallback?: boolean | 'rest' | 'proto'; + /** + * Fallback mode to use instead of gRPC. + * A string is accepted for compatibility, all non-empty string values enable the HTTP REST fallback. + */ + fallback?: boolean | string; } = {} ) { if (!isNodeJS()) { @@ -135,7 +138,7 @@ export class GrpcClient { (options.auth as GoogleAuth) || new GoogleAuth(options as GoogleAuthOptions); } - this.fallback = options.fallback !== 'rest' ? 'proto' : 'rest'; + this.fallback = options.fallback ? true : false; this.grpcVersion = require('../../package.json').version; this.httpRules = (options as GrpcClientOptions).httpRules; this.numericEnums = (options as GrpcClientOptions).numericEnums ?? false; @@ -316,14 +319,8 @@ export class GrpcClient { servicePort = 443; } - const encoder = - this.fallback === 'rest' - ? fallbackRest.encodeRequest - : fallbackProto.encodeRequest; - const decoder = - this.fallback === 'rest' - ? fallbackRest.decodeResponse - : fallbackProto.decodeResponse; + const encoder = fallbackRest.encodeRequest; + const decoder = fallbackRest.decodeResponse; const serviceStub = generateServiceStub( methods, protocol, @@ -362,7 +359,7 @@ export class GrpcClient { export function lro(options: GrpcClientOptions) { options = Object.assign({scopes: []}, options); if (options.protoJson) { - options = Object.assign(options, {fallback: 'rest'}); + options = Object.assign(options, {fallback: true}); } const gaxGrpc = new GrpcClient(options); return new OperationsClientBuilder(gaxGrpc, options.protoJson); @@ -396,10 +393,10 @@ export function createApiCall( func: Promise | GRPCCall, settings: gax.CallSettings, descriptor?: Descriptor, - fallback?: boolean | 'proto' | 'rest' + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _fallback?: boolean | string // unused; for compatibility only ): GaxCall { if ( - (!fallback || fallback === 'rest') && descriptor && 'streaming' in descriptor && (descriptor as StreamDescriptor).type !== StreamType.SERVER_STREAMING @@ -410,14 +407,10 @@ export function createApiCall( ); }; } - if ( - (fallback === 'proto' || fallback === true) && // for legacy reasons, fallback === true means 'proto' - descriptor && - 'streaming' in descriptor - ) { + if (descriptor && 'streaming' in descriptor && !isNodeJS()) { return () => { throw new Error( - 'The gRPC-fallback (proto over HTTP) transport currently does not support streaming calls.' + 'Server streaming over the REST transport is only supported in Node.js.' ); }; } diff --git a/src/fallbackProto.ts b/src/fallbackProto.ts deleted file mode 100644 index bfaa5fc10..000000000 --- a/src/fallbackProto.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright 2021 Google LLC - * - * 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. - */ - -// proto-over-HTTP request encoding and decoding - -import {defaultToObjectOptions} from './fallback'; -import {FetchParameters} from './fallbackServiceStub'; -import {GoogleErrorDecoder} from './googleError'; - -export function encodeRequest( - rpc: protobuf.Method, - protocol: string, - servicePath: string, - servicePort: number, - request: {} -): FetchParameters { - const protoNamespaces: string[] = []; - let currNamespace = rpc.parent!; - while (currNamespace.name !== '') { - protoNamespaces.unshift(currNamespace.name); - currNamespace = currNamespace.parent!; - } - const protoServiceName = protoNamespaces.join('.'); - const rpcName = rpc.name; - - const headers: {[key: string]: string} = { - 'Content-Type': 'application/x-protobuf', - }; - - const method = 'POST'; - const requestMessage = rpc.resolvedRequestType!.fromObject(request); - const body = rpc.resolvedRequestType!.encode(requestMessage).finish(); - const url = `${protocol}://${servicePath}:${servicePort}/$rpc/${protoServiceName}/${rpcName}`; - - return { - method, - url, - headers, - body, - }; -} - -export function decodeResponse( - rpc: protobuf.Method, - ok: boolean, - response: Buffer | ArrayBuffer -): {} { - if (!ok) { - const statusDecoder = new GoogleErrorDecoder(); - const error = statusDecoder.decodeErrorFromBuffer(response); - throw error; - } - - const buffer = - response instanceof ArrayBuffer ? new Uint8Array(response) : response; - const message = rpc.resolvedResponseType!.decode(buffer); - return rpc.resolvedResponseType!.toObject(message, defaultToObjectOptions); -} diff --git a/src/gax.ts b/src/gax.ts index 09e5d86ed..8497d53d8 100644 --- a/src/gax.ts +++ b/src/gax.ts @@ -67,19 +67,42 @@ import {toLowerCamelCase} from './util'; /** * Per-call configurable settings for retrying upon transient failure. + * @implements {RetryOptionsType} * @typedef {Object} RetryOptions - * @property {String[]} retryCodes + * @property {String[] | (function)} retryCodesOrShouldRetryFn * @property {BackoffSettings} backoffSettings + * @property {(function)} getResumptionRequestFn */ export class RetryOptions { - retryCodes: number[]; + retryCodesOrShouldRetryFn: number[] | ((error: any) => boolean); backoffSettings: BackoffSettings; - constructor(retryCodes: number[], backoffSettings: BackoffSettings) { - this.retryCodes = retryCodes; + getResumptionRequestFn?: (response: any) => any; + constructor( + retryCodesOrShouldRetryFn: number[] | ((error: any) => boolean), + backoffSettings: BackoffSettings, + getResumptionRequestFn?: (response: any) => any + ) { + this.retryCodesOrShouldRetryFn = retryCodesOrShouldRetryFn; this.backoffSettings = backoffSettings; + this.getResumptionRequestFn = getResumptionRequestFn; } } +/** + * Per-call configurable settings for working with retry-request + * See the repo README for more about the parameters + * https://github.com/googleapis/retry-request + * Will be deprecated in a future release. Only relevant to server streaming calls + * @typedef {Object} RetryOptions + * @property {boolean} objectMode - when true utilizes object mode in streams + * @property {request} request - the request to retry + * @property {number} noResponseRetries - number of times to retry on no response + * @property {number} currentRetryAttempt - what # retry attempt retry-request is on + * @property {Function} shouldRetryFn - determines whether to retry, returns a boolean + * @property {number} maxRetryDelay - maximum retry delay in seconds + * @property {number} retryDelayMultiplier - multiplier to increase the delay in between completion of failed requests + * @property {number} totalTimeout - total timeout in seconds + */ export interface RetryRequestOptions { objectMode?: boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -87,7 +110,10 @@ export interface RetryRequestOptions { retries?: number; noResponseRetries?: number; currentRetryAttempt?: number; - shouldRetryFn?: () => boolean; + shouldRetryFn?: (error: any) => boolean; + maxRetryDelay?: number; + retryDelayMultiplier?: number; + totalTimeout?: number; } /** @@ -209,31 +235,50 @@ export class CallSettings { let longrunning = this.longrunning; let apiName = this.apiName; let retryRequestOptions = this.retryRequestOptions; + // If a method-specific timeout is set in the service config, and the retry codes for that // method are non-null, then that timeout value will be used to // override backoff settings. - if ( - retry !== undefined && - retry !== null && - retry.retryCodes !== null && - retry.retryCodes.length > 0 - ) { - retry.backoffSettings.initialRpcTimeoutMillis = timeout; - retry.backoffSettings.maxRpcTimeoutMillis = timeout; - retry.backoffSettings.totalTimeoutMillis = timeout; + if (retry !== undefined && retry !== null) { + // if verify that the retry codes/retry function are not null or undefined + if ( + retry.retryCodesOrShouldRetryFn !== null && + retry.retryCodesOrShouldRetryFn !== undefined + ) { + // if it's an array of retry codes, make sure it has an element or check if it's a function + if ( + (retry.retryCodesOrShouldRetryFn instanceof Array && + retry.retryCodesOrShouldRetryFn.length > 0) || + retry.retryCodesOrShouldRetryFn instanceof Function + ) { + retry.backoffSettings.initialRpcTimeoutMillis = timeout; + retry.backoffSettings.maxRpcTimeoutMillis = timeout; + retry.backoffSettings.totalTimeoutMillis = timeout; + } + } } + // If the user provides a timeout to the method, that timeout value will be used // to override the backoff settings. if ('timeout' in options) { timeout = options.timeout!; - if ( - retry !== undefined && - retry !== null && - retry.retryCodes.length > 0 - ) { - retry.backoffSettings.initialRpcTimeoutMillis = timeout; - retry.backoffSettings.maxRpcTimeoutMillis = timeout; - retry.backoffSettings.totalTimeoutMillis = timeout; + if (retry !== undefined && retry !== null) { + // if verify that the retry codes/retry function are not null or undefined + if ( + retry.retryCodesOrShouldRetryFn !== null && + retry.retryCodesOrShouldRetryFn !== undefined + ) { + // if it's an array of retry codes, make sure it has an element or if it's a function + if ( + (retry.retryCodesOrShouldRetryFn instanceof Array && + retry.retryCodesOrShouldRetryFn.length > 0) || + retry.retryCodesOrShouldRetryFn instanceof Function + ) { + retry.backoffSettings.initialRpcTimeoutMillis = timeout; + retry.backoffSettings.maxRpcTimeoutMillis = timeout; + retry.backoffSettings.totalTimeoutMillis = timeout; + } + } } } if ('retry' in options) { @@ -262,7 +307,11 @@ export class CallSettings { isBundling = options.isBundling!; } - if ('maxRetries' in options) { + if ('maxRetries' in options && typeof options.maxRetries !== 'undefined') { + console.log( + 'removing timeout in favor of max retries', + retry!.backoffSettings!.totalTimeoutMillis + ); retry!.backoffSettings!.maxRetries = options.maxRetries; delete retry!.backoffSettings!.totalTimeoutMillis; } @@ -273,6 +322,7 @@ export class CallSettings { if ('apiName' in options) { apiName = options.apiName; } + // if ('retryRequestOptions' in options) { retryRequestOptions = options.retryRequestOptions; } @@ -292,23 +342,148 @@ export class CallSettings { } } +/** + * Validates passed retry options in preparation for eventual parameter deprecation + * converts retryRequestOptions to retryOptions + * then sets retryRequestOptions to null + * + * @param {CallOptions} options - a list of passed retry option + * @return {CallOptions} A new CallOptions object. + * + */ + +export function checkRetryOptions( + options?: CallOptions, + gaxStreamingRetries?: boolean +): CallOptions | undefined { + // options will be undefined if no CallOptions object is passed at call time + if (options) { + // if a user provided retry AND retryRequestOptions at call time, throw an error + if (gaxStreamingRetries) { + if ( + options.retry !== undefined && + options.retryRequestOptions !== undefined + ) { + throw new Error('Only one of retry or retryRequestOptions may be set'); + } else { + if (options.retryRequestOptions !== undefined) { + // // Retry settings + if (options.retryRequestOptions.objectMode) { + console.log( + 'objectMode override is not supported. It is set to true internally by default in gax.' + ); + } + if (options.retryRequestOptions.noResponseRetries) { + console.log( + 'noResponseRetries override is not supported. Please specify retry codes or a function to determine retry eligibility.' + ); + } + if (options.retryRequestOptions.currentRetryAttempt) { + console.log( + 'currentRetryAttempt override is not supported. Retry attempts are tracked internally.' + ); + } + let retryCodesOrShouldRetryFn; + + if (options.retryRequestOptions.shouldRetryFn) { + retryCodesOrShouldRetryFn = + options.retryRequestOptions.shouldRetryFn; + } else { + // default to retry code 14 per AIP-194 + retryCodesOrShouldRetryFn = [14]; + } + + //Backoff settings + if ( + options.retryRequestOptions.retries !== null && + options.retryRequestOptions !== undefined + ) { + // don't want to just check for truthiness here in case it's 0 + options.maxRetries = options.retryRequestOptions.retries; + } + // create a default backoff settings object in case the user didn't provide overrides for everything + const backoffSettings = createDefaultBackoffSettings(); + let maxRetryDelayMillis; + let totalTimeoutMillis; + // maxRetryDelay - this is in seconds, need to convert to milliseconds + if (options.retryRequestOptions.maxRetryDelay) { + maxRetryDelayMillis = + options.retryRequestOptions.maxRetryDelay * 1000; + } + // retryDelayMultiplier - should be a one to one mapping to retryDelayMultiplier + const retryDelayMultiplier = + options.retryRequestOptions.retryDelayMultiplier; + // totalTimeout - this is in seconds and needs to be converted to milliseconds and the totalTimeoutMillis parameter + if (options.retryRequestOptions.totalTimeout) { + totalTimeoutMillis = + options.retryRequestOptions.totalTimeout * 1000; + } + + // for the variables the user wants to override, override in the backoff settings object we made + if (maxRetryDelayMillis) { + backoffSettings.maxRetryDelayMillis = maxRetryDelayMillis; + } + if (retryDelayMultiplier) { + backoffSettings.retryDelayMultiplier = retryDelayMultiplier; + } + if (totalTimeoutMillis) { + backoffSettings.totalTimeoutMillis = totalTimeoutMillis; + } + + const convertedRetryOptions = createRetryOptions( + retryCodesOrShouldRetryFn, + backoffSettings + ); + options.retry = convertedRetryOptions; + delete options.retryRequestOptions; // completely remove them to avoid any further confusion + warn( + 'retry_request_options', + 'retryRequestOptions will be deprecated in a future release. Please use retryOptions to pass retry options at call time', + 'DeprecationWarning' + ); + } + } + } else { + // if user is opted into legacy settings but has passed retry settings, let them know there might be an issue if it's a streaming call + if (options.retry !== undefined) { + warn( + 'legacy_streaming_retry_behavior', + 'Legacy streaming retry behavior will not honor settings passed at call time or via client configuration. Please set gaxStreamingRetries to true to utilize passed retry settings. gaxStreamingRetries behavior will be set to true by default in future releases.', + 'DeprecationWarning' + ); + } + if (options.retryRequestOptions !== undefined) { + warn( + 'legacy_streaming_retry_request_behavior', + 'Legacy streaming retry behavior will not honor retryRequestOptions passed at call time. Please set gaxStreamingRetries to true to utilize passed retry settings. gaxStreamingRetries behavior will convert retryRequestOptions to retry parameters by default in future releases.', + 'DeprecationWarning' + ); + } + } + } + return options; +} + /** * Per-call configurable settings for retrying upon transient failure. * - * @param {number[]} retryCodes - a list of Google API canonical error codes + * @param {number[] | function} retryCodesOrShouldRetryFn - a list of Google API canonical error codes OR a function that returns a boolean to determine retry behavior * upon which a retry should be attempted. * @param {BackoffSettings} backoffSettings - configures the retry * exponential backoff algorithm. + * @param {function} getResumptionRequestFn - a function with a resumption strategy - only used with server streaming retries * @return {RetryOptions} A new RetryOptions object. * */ export function createRetryOptions( - retryCodes: number[], - backoffSettings: BackoffSettings + retryCodesOrShouldRetryFn: number[] | ((response: any) => boolean), + backoffSettings: BackoffSettings, + getResumptionRequestFn?: (response: any) => any ): RetryOptions { return { - retryCodes, + retryCodesOrShouldRetryFn, backoffSettings, + getResumptionRequestFn, }; } @@ -479,7 +654,7 @@ function constructRetry( return null; } - let codes: number[] | null = null; + let codes: number[] | null = null; // this is one instance where it will NOT be an array OR a function because we do not allow shouldRetryFn in the client if (retryCodes && 'retry_codes_name' in methodConfig) { const retryCodesName = methodConfig['retry_codes_name']; codes = (retryCodes[retryCodesName!] || []).map(name => { @@ -526,16 +701,30 @@ function mergeRetryOptions( return null; } - if (!overrides.retryCodes && !overrides.backoffSettings) { + if ( + !overrides.retryCodesOrShouldRetryFn && + !overrides.backoffSettings && + !overrides.getResumptionRequestFn + ) { return retry; } - const codes = overrides.retryCodes ? overrides.retryCodes : retry.retryCodes; + const codesOrFunction = overrides.retryCodesOrShouldRetryFn + ? overrides.retryCodesOrShouldRetryFn + : retry.retryCodesOrShouldRetryFn; const backoffSettings = overrides.backoffSettings ? overrides.backoffSettings : retry.backoffSettings; - return createRetryOptions(codes!, backoffSettings!); + + const getResumptionRequestFn = overrides.getResumptionRequestFn + ? overrides.getResumptionRequestFn + : retry.getResumptionRequestFn; + return createRetryOptions( + codesOrFunction!, + backoffSettings!, + getResumptionRequestFn! + ); } export interface ServiceConfig { diff --git a/src/iamService.ts b/src/iamService.ts index 2a3df9ba6..758536775 100644 --- a/src/iamService.ts +++ b/src/iamService.ts @@ -203,7 +203,7 @@ export class IamClient { getIamPolicy( request: protos.google.iam.v1.GetIamPolicyRequest, options?: gax.CallOptions - ): Promise; + ): Promise<[protos.google.iam.v1.Policy]>; getIamPolicy( request: protos.google.iam.v1.GetIamPolicyRequest, options: gax.CallOptions, @@ -235,7 +235,7 @@ export class IamClient { protos.google.iam.v1.GetIamPolicyRequest | null | undefined, {} | null | undefined > - ): Promise { + ): Promise<[protos.google.iam.v1.Policy]> { let options: gax.CallOptions; if (optionsOrCallback instanceof Function && callback === undefined) { callback = optionsOrCallback as unknown as Callback< @@ -262,7 +262,7 @@ export class IamClient { setIamPolicy( request: protos.google.iam.v1.SetIamPolicyRequest, options?: gax.CallOptions - ): Promise; + ): Promise<[protos.google.iam.v1.Policy]>; setIamPolicy( request: protos.google.iam.v1.SetIamPolicyRequest, options: gax.CallOptions, @@ -294,7 +294,7 @@ export class IamClient { protos.google.iam.v1.SetIamPolicyRequest | null | undefined, {} | null | undefined > - ): Promise { + ): Promise<[protos.google.iam.v1.Policy]> { let options: gax.CallOptions; if (optionsOrCallback instanceof Function && callback === undefined) { callback = optionsOrCallback as unknown as Callback< @@ -320,7 +320,7 @@ export class IamClient { testIamPermissions( request: protos.google.iam.v1.TestIamPermissionsRequest, options?: gax.CallOptions - ): Promise; + ): Promise<[protos.google.iam.v1.TestIamPermissionsResponse]>; testIamPermissions( request: protos.google.iam.v1.TestIamPermissionsRequest, callback: Callback< @@ -352,7 +352,7 @@ export class IamClient { protos.google.iam.v1.TestIamPermissionsRequest | null | undefined, {} | null | undefined > - ): Promise { + ): Promise<[protos.google.iam.v1.TestIamPermissionsResponse]> { let options: gax.CallOptions; if (optionsOrCallback instanceof Function && callback === undefined) { callback = optionsOrCallback as unknown as Callback< @@ -408,7 +408,7 @@ export interface IamClient { protos.google.iam.v1.GetIamPolicyRequest | null | undefined, {} | null | undefined > - ): Promise; + ): Promise<[protos.google.iam.v1.Policy]>; setIamPolicy(request: protos.google.iam.v1.SetIamPolicyRequest): void; setIamPolicy( request: protos.google.iam.v1.SetIamPolicyRequest, @@ -424,7 +424,7 @@ export interface IamClient { protos.google.iam.v1.SetIamPolicyRequest | null | undefined, {} | null | undefined > - ): Promise; + ): Promise<[protos.google.iam.v1.Policy]>; testIamPermissions( request: protos.google.iam.v1.TestIamPermissionsRequest ): void; @@ -442,5 +442,5 @@ export interface IamClient { protos.google.iam.v1.TestIamPermissionsRequest | null | undefined, {} | null | undefined > - ): Promise; + ): Promise<[protos.google.iam.v1.TestIamPermissionsResponse]>; } diff --git a/src/normalCalls/retries.ts b/src/normalCalls/retries.ts index 2a6e6ab52..e00e5087d 100644 --- a/src/normalCalls/retries.ts +++ b/src/normalCalls/retries.ts @@ -107,7 +107,11 @@ export function retryable( return; } canceller = null; - if (retry.retryCodes.indexOf(err!.code!) < 0) { + if ( + retry.retryCodesOrShouldRetryFn !== undefined && + retry.retryCodesOrShouldRetryFn instanceof Array && + retry.retryCodesOrShouldRetryFn.indexOf(err!.code!) < 0 + ) { err.note = 'Exception occurred in retry method that was ' + 'not classified as transient'; diff --git a/src/normalCalls/timeout.ts b/src/normalCalls/timeout.ts index b1790d854..f703e3b14 100644 --- a/src/normalCalls/timeout.ts +++ b/src/normalCalls/timeout.ts @@ -17,6 +17,7 @@ import { GRPCCall, GRPCCallOtherArgs, + ServerStreamingCall, SimpleCallbackFunction, UnaryCall, } from '../apitypes'; @@ -54,3 +55,22 @@ export function addTimeoutArg( return (func as UnaryCall)(argument, metadata!, options, callback); }; } + +export function addServerTimeoutArg( + func: GRPCCall, + timeout: number, + otherArgs: GRPCCallOtherArgs, + abTests?: {} +): SimpleCallbackFunction { + // TODO: this assumes the other arguments consist of metadata and options, + // which is specific to gRPC calls. Remove the hidden dependency on gRPC. + return argument => { + const now = new Date(); + const options = otherArgs.options || {}; + options.deadline = new Date(now.getTime() + timeout); + const metadata = otherArgs.metadataBuilder + ? otherArgs.metadataBuilder(abTests, otherArgs.headers || {}) + : null; + return (func as ServerStreamingCall)(argument, metadata!, options); + }; +} diff --git a/src/paginationCalls/pageDescriptor.ts b/src/paginationCalls/pageDescriptor.ts index a4bf867b1..6bc0a9c46 100644 --- a/src/paginationCalls/pageDescriptor.ts +++ b/src/paginationCalls/pageDescriptor.ts @@ -81,7 +81,11 @@ export class PageDescriptor implements Descriptor { // emit full api response with every page. stream.emit('response', apiResp); for (let i = 0; i < resources.length; ++i) { - if ((stream as any)._readableState.ended) { + // TODO: rewrite without accessing stream internals + if ( + (stream as unknown as {_readableState: {ended: boolean}}) + ._readableState.ended + ) { return; } if (resources[i] === null) { @@ -93,7 +97,11 @@ export class PageDescriptor implements Descriptor { stream.end(); } } - if ((stream as any)._readableState.ended) { + // TODO: rewrite without accessing stream internals + if ( + (stream as unknown as {_readableState: {ended: boolean}})._readableState + .ended + ) { return; } if (!next) { diff --git a/src/protosList.json b/src/protosList.json index 3990c90fc..631558bab 100644 --- a/src/protosList.json +++ b/src/protosList.json @@ -1,5 +1,7 @@ [ "google/api/annotations.proto", + "google/api/apikeys/v2/apikeys.proto", + "google/api/apikeys/v2/resources.proto", "google/api/auth.proto", "google/api/backend.proto", "google/api/billing.proto", @@ -45,6 +47,7 @@ "google/api/servicecontrol/v1/operation.proto", "google/api/servicecontrol/v1/quota_controller.proto", "google/api/servicecontrol/v1/service_controller.proto", + "google/api/servicecontrol/v2/service_controller.proto", "google/api/servicemanagement/v1/resources.proto", "google/api/servicemanagement/v1/servicemanager.proto", "google/api/serviceusage/v1/resources.proto", @@ -77,11 +80,14 @@ "google/monitoring/v3/query_service.proto", "google/monitoring/v3/service.proto", "google/monitoring/v3/service_service.proto", + "google/monitoring/v3/snooze.proto", + "google/monitoring/v3/snooze_service.proto", "google/monitoring/v3/span_context.proto", "google/monitoring/v3/uptime.proto", "google/monitoring/v3/uptime_service.proto", "google/protobuf/any.proto", "google/protobuf/api.proto", + "google/protobuf/bridge/message_set.proto", "google/protobuf/compiler/plugin.proto", "google/protobuf/compiler/ruby/ruby_generated_code.proto", "google/protobuf/compiler/ruby/ruby_generated_code_proto2.proto", @@ -102,7 +108,9 @@ "google/protobuf/wrappers.proto", "google/rpc/code.proto", "google/rpc/context/attribute_context.proto", + "google/rpc/context/audit_context.proto", "google/rpc/error_details.proto", + "google/rpc/http.proto", "google/rpc/status.proto", "google/type/calendar_period.proto", "google/type/color.proto", diff --git a/src/streamArrayParser.ts b/src/streamArrayParser.ts index 697dc2521..7aa03c1f4 100644 --- a/src/streamArrayParser.ts +++ b/src/streamArrayParser.ts @@ -116,7 +116,7 @@ export class StreamArrayParser extends Transform { chunk.slice(objectStart, curIndex + 1), ]); try { - // HTTP reponse.ok is true. + // HTTP response.ok is true. const msgObj = decodeResponse(this.rpc, true, objBuff); this.push(msgObj); } catch (err) { diff --git a/src/streamingCalls/streamDescriptor.ts b/src/streamingCalls/streamDescriptor.ts index d23f80219..569374d31 100644 --- a/src/streamingCalls/streamDescriptor.ts +++ b/src/streamingCalls/streamDescriptor.ts @@ -17,7 +17,6 @@ import {APICaller} from '../apiCaller'; import {Descriptor} from '../descriptor'; import {CallSettings} from '../gax'; - import {StreamType} from './streaming'; import {StreamingApiCaller} from './streamingApiCaller'; @@ -28,19 +27,23 @@ export class StreamDescriptor implements Descriptor { type: StreamType; streaming: boolean; // needed for browser support rest?: boolean; + gaxStreamingRetries?: boolean; - constructor(streamType: StreamType, rest?: boolean) { + constructor( + streamType: StreamType, + rest?: boolean, + gaxStreamingRetries?: boolean + ) { this.type = streamType; this.streaming = true; this.rest = rest; + this.gaxStreamingRetries = gaxStreamingRetries; } getApiCaller(settings: CallSettings): APICaller { // Right now retrying does not work with gRPC-streaming, because retryable // assumes an API call returns an event emitter while gRPC-streaming methods // return Stream. - // TODO: support retrying. - settings.retry = null; return new StreamingApiCaller(this); } } diff --git a/src/streamingCalls/streaming.ts b/src/streamingCalls/streaming.ts index ec3192160..e5b18ecf6 100644 --- a/src/streamingCalls/streaming.ts +++ b/src/streamingCalls/streaming.ts @@ -24,8 +24,11 @@ import { GRPCCallResult, SimpleCallbackFunction, } from '../apitypes'; -import {RetryRequestOptions} from '../gax'; +import {RetryOptions, RetryRequestOptions} from '../gax'; import {GoogleError} from '../googleError'; +import {streamingRetryRequest} from '../streamingRetryRequest'; +import {Status} from '../status'; +import {warn} from '../warnings'; // eslint-disable-next-line @typescript-eslint/no-var-requires const duplexify: DuplexifyConstructor = require('duplexify'); @@ -84,6 +87,11 @@ export class StreamProxy extends duplexify implements GRPCCallResult { stream?: CancellableStream; private _responseHasSent: boolean; rest?: boolean; + new_retry?: boolean; + apiCall?: SimpleCallbackFunction; + argument?: {}; + prevDeadline?: number; + retries?: number = 0; /** * StreamProxy is a proxy to gRPC-streaming method. * @@ -92,7 +100,12 @@ export class StreamProxy extends duplexify implements GRPCCallResult { * @param {StreamType} type - the type of gRPC stream. * @param {ApiCallback} callback - the callback for further API call. */ - constructor(type: StreamType, callback: APICallback, rest?: boolean) { + constructor( + type: StreamType, + callback: APICallback, + rest?: boolean, + new_retry?: boolean + ) { super(undefined, undefined, { objectMode: true, readable: type !== StreamType.CLIENT_STREAMING, @@ -103,6 +116,7 @@ export class StreamProxy extends duplexify implements GRPCCallResult { this._isCancelCalled = false; this._responseHasSent = false; this.rest = rest; + this.new_retry = new_retry; } cancel() { @@ -113,10 +127,192 @@ export class StreamProxy extends duplexify implements GRPCCallResult { } } + retry(stream: CancellableStream, retry: RetryOptions) { + let retryArgument = this.argument!; + + if ( + typeof retry.getResumptionRequestFn! === 'function' && + retry.getResumptionRequestFn!(this.argument) + ) { + retryArgument = retry.getResumptionRequestFn!(this.argument); + } + + this.resetStreams(stream); + + const new_stream = this.apiCall!( + retryArgument, + this._callback + ) as CancellableStream; + this.stream = new_stream; + + const retryStream = this.streamHandoffHelper(new_stream, retry); + if (retryStream !== undefined) { + return retryStream; + } + return new_stream; + } + + /** + * Helper function to handle total timeout + max retry check for server streaming retries + * @param {number} deadline - the current retry deadline + * @param {number} maxRetries - maximum total number of retries + * @param {number} totalTimeoutMillis - total timeout in milliseconds + */ + timeoutAndMaxRetryCheck( + deadline: number, + maxRetries: number, + totalTimeoutMillis: number + ): void { + const now = new Date(); + + if ( + this.prevDeadline! !== undefined && + deadline && + now.getTime() >= this.prevDeadline + ) { + const error = new GoogleError( + `Total timeout of API exceeded ${totalTimeoutMillis} milliseconds before any response was received.` + ); + error.code = Status.DEADLINE_EXCEEDED; + this.emit('error', error); + + this.destroy(error); + // Without throwing error you get unhandled error since we are returning a new stream + // There might be a better way to do this + throw error; + } + + if (this.retries && this.retries > maxRetries) { + const error = new GoogleError( + 'Exceeded maximum number of retries before any ' + + 'response was received' + ); + error.code = Status.DEADLINE_EXCEEDED; + this.emit('error', error); + + this.destroy(error); + throw error; + } + } + + /** + * Error handler for server streaming retries + * @param {CancellableStream} stream - the stream being retried + * @param {RetryOptions} retry - Configures the exceptions upon which the + * function should retry, and the parameters to the exponential backoff retry + * algorithm. + * @param {Error} error - error to handle + */ + streamHandoffErrorHandler( + stream: CancellableStream, + retry: RetryOptions, + error: Error + ): void { + let retryStream = this.stream; + const delayMult = retry.backoffSettings.retryDelayMultiplier; + const maxDelay = retry.backoffSettings.maxRetryDelayMillis; + const timeoutMult = retry.backoffSettings.rpcTimeoutMultiplier; + const maxTimeout = retry.backoffSettings.maxRpcTimeoutMillis; + + let delay = retry.backoffSettings.initialRetryDelayMillis; + let timeout = retry.backoffSettings.initialRpcTimeoutMillis; + let now = new Date(); + let deadline = 0; + + if (retry.backoffSettings.totalTimeoutMillis) { + deadline = now.getTime() + retry.backoffSettings.totalTimeoutMillis; + } + const maxRetries = retry.backoffSettings.maxRetries!; + + this.timeoutAndMaxRetryCheck( + deadline, + maxRetries, + retry.backoffSettings.totalTimeoutMillis! + ); + + this.retries!++; + + const e = GoogleError.parseGRPCStatusDetails(error); + let shouldRetry = this.defaultShouldRetry(e!, retry); + if (typeof retry.retryCodesOrShouldRetryFn! === 'function') { + shouldRetry = retry.retryCodesOrShouldRetryFn!(e!); + } + + if (shouldRetry) { + const toSleep = Math.random() * delay; + setTimeout(() => { + now = new Date(); + delay = Math.min(delay * delayMult, maxDelay); + const timeoutCal = timeout && timeoutMult ? timeout * timeoutMult : 0; + const rpcTimeout = maxTimeout ? maxTimeout : 0; + this.prevDeadline = deadline; + const newDeadline = deadline ? deadline - now.getTime() : 0; + timeout = Math.min(timeoutCal, rpcTimeout, newDeadline); + }, toSleep); + } else { + const newError = new GoogleError( + 'Exception occurred in retry method that was ' + + 'not classified as transient' + ); + newError.code = Status.INVALID_ARGUMENT; + this.emit('error', newError); + this.destroy(newError); + return; + } + + if (maxRetries && deadline!) { + const newError = new GoogleError( + 'Cannot set both totalTimeoutMillis and maxRetries ' + + 'in backoffSettings.' + ); + newError.code = Status.INVALID_ARGUMENT; + this.emit('error', newError); + + this.destroy(newError); + } else { + retryStream = this.retry(stream, retry); + this.stream = retryStream; + return; + } + } + + /** + * Used during server streaming retries to handle + * event forwarding, errors, and/or stream closure + * @param {CancellableStream} stream - the stream that we're doing the retry on + * @param {RetryOptions} retry - Configures the exceptions upon which the + * function should retry, and the parameters to the exponential backoff retry + * algorithm. + */ + streamHandoffHelper(stream: CancellableStream, retry: RetryOptions): void { + let enteredError = false; + const eventsToForward = ['metadata', 'response', 'status', 'data']; + eventsToForward.forEach(event => { + stream.on(event, this.emit.bind(this, event)); + }); + + stream.on('error', error => { + enteredError = true; + this.streamHandoffErrorHandler(stream, retry, error); + }); + + stream.on('end', () => { + if (!enteredError) { + enteredError = true; + this.emit('end'); + this.cancel(); + } + }); + } + /** * Forward events from an API request stream to the user's stream. * @param {Stream} stream - The API request stream. + * @param {RetryOptions} retry - Configures the exceptions upon which the + * function should retry, and the parameters to the exponential backoff retry + * algorithm. */ + forwardEvents(stream: Stream) { const eventsToForward = ['metadata', 'response', 'status']; eventsToForward.forEach(event => { @@ -158,21 +354,169 @@ export class StreamProxy extends duplexify implements GRPCCallResult { }); } + defaultShouldRetry(error: GoogleError, retry: RetryOptions) { + if ( + retry.retryCodesOrShouldRetryFn instanceof Array && + retry.retryCodesOrShouldRetryFn.indexOf(error!.code!) < 0 + ) { + return false; + } + return true; + } + + /** + * Forward events from an API request stream to the user's stream. + * @param {Stream} stream - The API request stream. + * @param {RetryOptions} retry - Configures the exceptions upon which the + * function eshould retry, and the parameters to the exponential backoff retry + * algorithm. + */ + forwardEventsNewImplementation( + stream: CancellableStream, + retry: RetryOptions + ): CancellableStream | undefined { + let retryStream = this.stream; + const eventsToForward = ['metadata', 'response', 'status']; + eventsToForward.forEach(event => { + stream.on(event, this.emit.bind(this, event)); + }); + // gRPC is guaranteed emit the 'status' event but not 'metadata', and 'status' is the last event to emit. + // Emit the 'response' event if stream has no 'metadata' event. + // This avoids the stream swallowing the other events, such as 'end'. + stream.on('status', () => { + if (!this._responseHasSent) { + stream.emit('response', { + code: 200, + details: '', + message: 'OK', + }); + } + }); + + // We also want to supply the status data as 'response' event to support + // the behavior of google-cloud-node expects. + // see: + // https://github.com/GoogleCloudPlatform/google-cloud-node/pull/1775#issuecomment-259141029 + // https://github.com/GoogleCloudPlatform/google-cloud-node/blob/116436fa789d8b0f7fc5100b19b424e3ec63e6bf/packages/common/src/grpc-service.js#L355 + stream.on('metadata', metadata => { + // Create a response object with succeeds. + // TODO: unify this logic with the decoration of gRPC response when it's + // added. see: https://github.com/googleapis/gax-nodejs/issues/65 + stream.emit('response', { + code: 200, + details: '', + message: 'OK', + metadata, + }); + this._responseHasSent = true; + }); + + stream.on('error', error => { + const timeout = retry.backoffSettings.totalTimeoutMillis; + const maxRetries = retry.backoffSettings.maxRetries!; + if ((maxRetries && maxRetries > 0) || (timeout && timeout > 0)) { + const e = GoogleError.parseGRPCStatusDetails(error); + let shouldRetry = this.defaultShouldRetry(e!, retry); + if (typeof retry.retryCodesOrShouldRetryFn! === 'function') { + shouldRetry = retry.retryCodesOrShouldRetryFn!(e!); + } + + if (shouldRetry) { + if (maxRetries && timeout!) { + const newError = new GoogleError( + 'Cannot set both totalTimeoutMillis and maxRetries ' + + 'in backoffSettings.' + ); + newError.code = Status.INVALID_ARGUMENT; + this.emit('error', newError); + this.destroy(newError); + return; + } else { + retryStream = this.retry(stream, retry); + this.stream = retryStream; + return retryStream; + } + } else { + const newError = new GoogleError( + 'Exception occurred in retry method that was ' + + 'not classified as transient' + ); + newError.code = Status.INVALID_ARGUMENT; + this.emit('error', newError); + this.destroy(newError); + return; + } + } else { + return GoogleError.parseGRPCStatusDetails(error); + } + }); + return retryStream; + } + + resetStreams(requestStream: CancellableStream) { + if (requestStream) { + requestStream.cancel && requestStream.cancel(); + + if (requestStream.destroy) { + requestStream.destroy(); + } else if (requestStream.end) { + requestStream.end(); + } + } + } + /** * Specifies the target stream. * @param {ApiCall} apiCall - the API function to be called. * @param {Object} argument - the argument to be passed to the apiCall. + * @param {RetryOptions} retry - Configures the exceptions upon which the + * function eshould retry, and the parameters to the exponential backoff retry + * algorithm. */ setStream( apiCall: SimpleCallbackFunction, argument: {}, - retryRequestOptions: RetryRequestOptions = {} + retryRequestOptions: RetryRequestOptions = {}, + retry: RetryOptions ) { + this.apiCall = apiCall; + this.argument = argument; + if (this.type === StreamType.SERVER_STREAMING) { if (this.rest) { const stream = apiCall(argument, this._callback) as CancellableStream; this.stream = stream; this.setReadable(stream); + } else if (this.new_retry) { + warn( + 'gax_server_streaming_retries', + 'You are using the new experimental gax native server streaming retry implementation', + 'ExperimentalWarning' + ); + const retryStream = streamingRetryRequest( + null, + { + objectMode: true, + request: () => { + if (this._isCancelCalled) { + if (this.stream) { + this.stream.cancel(); + } + return; + } + const stream = apiCall( + argument, + this._callback + ) as CancellableStream; + this.stream = stream; + this.stream = this.forwardEventsNewImplementation(stream, retry); + return this.stream; + }, + }, + null + ); + + this.setReadable(retryStream); } else { const retryStream = retryRequest(null, { objectMode: true, @@ -203,7 +547,12 @@ export class StreamProxy extends duplexify implements GRPCCallResult { const stream = apiCall(argument, this._callback) as CancellableStream; this.stream = stream; - this.forwardEvents(stream); + + if (this.new_retry) { + this.forwardEventsNewImplementation(stream, retry); + } else { + this.forwardEvents(stream); + } if (this.type === StreamType.CLIENT_STREAMING) { this.setWritable(stream); diff --git a/src/streamingCalls/streamingApiCaller.ts b/src/streamingCalls/streamingApiCaller.ts index 6f8dc3634..68ddb9855 100644 --- a/src/streamingCalls/streamingApiCaller.ts +++ b/src/streamingCalls/streamingApiCaller.ts @@ -47,7 +47,8 @@ export class StreamingApiCaller implements APICaller { return new StreamProxy( this.descriptor.type, callback, - this.descriptor.rest + this.descriptor.rest, + this.descriptor.gaxStreamingRetries ); } @@ -85,7 +86,12 @@ export class StreamingApiCaller implements APICaller { settings: CallSettings, stream: StreamProxy ) { - stream.setStream(apiCall, argument, settings.retryRequestOptions); + stream.setStream( + apiCall, + argument, + settings.retryRequestOptions, + settings.retry! + ); } fail(stream: CancellableStream, err: Error) { diff --git a/src/streamingRetryRequest.ts b/src/streamingRetryRequest.ts new file mode 100644 index 000000000..0e32ff70e --- /dev/null +++ b/src/streamingRetryRequest.ts @@ -0,0 +1,205 @@ +// Copyright 2023 Google LLC + +// 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 + +// https://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. + +const {PassThrough} = require('stream'); +const extend = require('extend'); + +const DEFAULTS = { + /* + Max # of retries + */ + maxRetries: 2, + + /* + The maximum time to delay in seconds. If retryDelayMultiplier results in a + delay greater than maxRetryDelay, retries should delay by maxRetryDelay + seconds instead. + */ + maxRetryDelayMillis: 64000, + + /* + The multiplier by which to increase the delay time between the completion of + failed requests, and the initiation of the subsequent retrying request. + */ + retryDelayMultiplier: 2, + + /* + The length of time to keep retrying in seconds. The last sleep period will + be shortened as necessary, so that the last retry runs at deadline (and not + considerably beyond it). The total time starting from when the initial + request is sent, after which an error will be returned, regardless of the + retrying attempts made meanwhile. + */ + totalTimeoutMillis: 600000, + + /* + The initial delay time, in milliseconds, between the completion of the first + failed request and the initiation of the first retrying request. + */ + initialRetryDelayMillis: 60, + + /* + Initial timeout parameter to the request. + */ + initialRpcTimeoutMillis: 60, + + /* + Maximum timeout in milliseconds for a request. + When this value is reached, rpcTimeoutMulitplier will no + longer be used to increase the timeout. + */ + maxRpcTimeoutMillis: 60, + + /* + Multiplier by which to increase timeout parameter in + between failed requests. + */ + rpcTimeoutMultiplier: 2, + + /* + The number of retries that have occured. + */ + retries: 0, + + retryCodesOrShouldRetryFn: + [14] || + function (response: any) { + return undefined; + }, + + getResumptionRequestFn: function (response: any) { + return undefined; + }, +}; + +export function streamingRetryRequest( + requestOpts: any = null, + opts: any = null, + callback: any = null, + ...args: any +) { + const streamMode = typeof args[args.length - 1] !== 'function'; + + if (typeof opts === 'function') { + callback = opts; + } + + opts = extend({}, DEFAULTS, opts); + + if (typeof opts.request === 'undefined') { + try { + // eslint-disable-next-line node/no-unpublished-require + opts.request = require('request'); + } catch (e) { + throw new Error('A request library must be provided to retry-request.'); + } + } + + let numNoResponseAttempts = 0; + let streamResponseHandled = false; + + let retryStream: any; + let requestStream: any; + let delayStream: any; + + let activeRequest: {abort: () => void}; + const retryRequest = { + abort: function () { + if (activeRequest && activeRequest.abort) { + activeRequest.abort(); + } + }, + }; + + if (streamMode) { + retryStream = new PassThrough({objectMode: opts.objectMode}); + // retryStream.abort = resetStreams; + } + + makeRequest(); + + if (streamMode) { + return retryStream; + } else { + return retryRequest; + } + + function makeRequest() { + if (streamMode) { + streamResponseHandled = false; + + delayStream = new PassThrough({objectMode: opts.objectMode}); + requestStream = opts.request(requestOpts); + + setImmediate(() => { + retryStream.emit('request'); + }); + + requestStream + // gRPC via google-cloud-node can emit an `error` as well as a `response` + // Whichever it emits, we run with-- we can't run with both. That's what + // is up with the `streamResponseHandled` tracking. + .on('error', (err: any) => { + if (streamResponseHandled) { + return; + } + streamResponseHandled = true; + onResponse(err); + }) + .on('response', (resp: any, body: any) => { + if (streamResponseHandled) { + return; + } + + streamResponseHandled = true; + onResponse(null, resp, body); + }) + .on('complete', retryStream.emit.bind(retryStream, 'complete')); + + requestStream.pipe(delayStream); + } else { + activeRequest = opts.request(requestOpts, onResponse); + } + } + + function onResponse(err: any, response: any = null, body: any = null) { + // An error such as DNS resolution. + if (err) { + numNoResponseAttempts++; + + if (numNoResponseAttempts <= opts.maxRetries) { + makeRequest(); + } else { + if (streamMode) { + retryStream.emit('error', err); + } else { + callback(err, response, body); + } + } + + return; + } + + // No more attempts need to be made, just continue on. + if (streamMode) { + retryStream.emit('response', response); + delayStream.pipe(retryStream); + requestStream.on('error', (err: any) => { + retryStream.destroy(err); + }); + } else { + callback(err, response, body); + } + } +} diff --git a/test/browser-test/karma.conf.js b/test/browser-test/karma.conf.js index f34eacc0c..5754262b1 100644 --- a/test/browser-test/karma.conf.js +++ b/test/browser-test/karma.conf.js @@ -86,7 +86,9 @@ module.exports = function (config) { base: 'ChromeHeadless', // We must disable the Chrome sandbox when running Chrome inside Docker (Chrome's sandbox needs // more permissions than Docker allows by default) - flags: isDocker ? ['--no-sandbox'] : [], + flags: isDocker + ? ['--no-sandbox', '--disable-web-security'] + : ['--disable-web-security'], }, }, diff --git a/test/browser-test/package.json b/test/browser-test/package.json index f98e7d39b..5171769db 100644 --- a/test/browser-test/package.json +++ b/test/browser-test/package.json @@ -7,6 +7,9 @@ "files": [ "build/test" ], + "engines": { + "node": ">=14" + }, "license": "Apache-2.0", "keywords": [], "scripts": { diff --git a/test/browser-test/test/test.endtoend.ts b/test/browser-test/test/test.endtoend.ts index f07cb3213..1d9556dbc 100644 --- a/test/browser-test/test/test.endtoend.ts +++ b/test/browser-test/test/test.endtoend.ts @@ -41,7 +41,7 @@ describe('Run tests against gRPC server', () => { const opts = { auth: authStub as unknown as GoogleAuth, protocol: 'http', - port: 1337, + port: 7469, }; // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -87,8 +87,11 @@ describe('Run tests against gRPC server', () => { const words = ['nobody', 'ever', 'reads', 'test', 'input']; const request = { content: words.join(' '), + pageSize: 2, }; - assert.throws(() => client.expand(request)); + assert.throws(() => { + client.expand(request); + }); }); it('should be able to call paging calls', async () => { diff --git a/test/browser-test/test/test.grpc-fallback.ts b/test/browser-test/test/test.grpc-fallback.ts index d9c9133b3..6cf85e725 100644 --- a/test/browser-test/test/test.grpc-fallback.ts +++ b/test/browser-test/test/test.grpc-fallback.ts @@ -23,7 +23,6 @@ import {protobuf, GoogleAuth, fallback} from 'google-gax'; import {EchoClient} from 'showcase-echo-client'; import echoProtoJson = require('showcase-echo-client/build/protos/protos.json'); -import statusProtoJson = require('google-gax/build/protos/status.json'); const authStub = { getClient: async () => { @@ -193,12 +192,13 @@ describe('grpc-fallback', () => { it('should make a request', async () => { const client = new EchoClient(opts); const requestObject = {content: 'test-content'}; - const responseType = protos.lookupType('EchoResponse'); - const response = responseType.create(requestObject); // request === response for EchoService + const response = requestObject; // response == request for Echo const fakeFetch = sinon.fake.resolves({ ok: true, arrayBuffer: () => { - return Promise.resolve(responseType.encode(response).finish()); + return Promise.resolve( + new TextEncoder().encode(JSON.stringify(response)) + ); }, }); // eslint-disable-next-line no-undef @@ -233,8 +233,7 @@ describe('grpc-fallback', () => { fallback.routingHeader.fromParams({ abc: 'def', }); - const responseType = protos.lookupType('EchoResponse'); - const response = responseType.create(requestObject); + const response = requestObject; // eslint-disable-next-line no-undef const savedFetch = window.fetch; // @ts-ignore @@ -245,7 +244,9 @@ describe('grpc-fallback', () => { return Promise.resolve({ ok: true, arrayBuffer: () => { - return Promise.resolve(responseType.encode(response).finish()); + return Promise.resolve( + new TextEncoder().encode(JSON.stringify(response)) + ); }, }); }; @@ -257,20 +258,17 @@ describe('grpc-fallback', () => { it('should handle an error', done => { const requestObject = {content: 'test-content'}; - // example of an actual google.rpc.Status error message returned by Language - // API const expectedError = Object.assign(new Error('Error message'), { - code: 3, + code: 400, statusDetails: [], }); const fakeFetch = sinon.fake.resolves({ ok: false, arrayBuffer: () => { - const root = protobuf.Root.fromJSON(statusProtoJson); - const statusType = root.lookupType('google.rpc.Status'); - const statusMessage = statusType.fromObject(expectedError); - return Promise.resolve(statusType.encode(statusMessage).finish()); + return Promise.resolve( + new TextEncoder().encode(JSON.stringify(expectedError)) + ); }, }); // eslint-disable-next-line no-undef @@ -279,8 +277,6 @@ describe('grpc-fallback', () => { gaxGrpc.createStub(echoService, stubOptions).then(echoStub => { echoStub.echo(requestObject, {}, {}, (err?: Error) => { assert(err instanceof Error); - assert.strictEqual(err.message, '3 INVALID_ARGUMENT: Error message'); - assert.strictEqual(JSON.stringify(err), JSON.stringify(expectedError)); done(); }); }); diff --git a/test/showcase-echo-client/package.json b/test/showcase-echo-client/package.json index aab2afc7e..4e4a64b95 100644 --- a/test/showcase-echo-client/package.json +++ b/test/showcase-echo-client/package.json @@ -1,5 +1,5 @@ { - "name": "showcase-echo-client", + "name": "showcase-echo-client", "version": "0.1.0", "description": "Showcase client for Node.js", "repository": "googleapis/nodejs-showcase", @@ -21,46 +21,23 @@ "cloud", "google showcase", "showcase", - "compliance", "echo", - "identity", "sequence service" ], "scripts": { - "compile": "tsc -p . && cp -r protos build/", + "clean": "gts clean", + "compile": "tsc -p . && cp -r protos build/ && minifyProtoJson", "compile-protos": "compileProtos src", - "docs": "jsdoc -c .jsdoc.js", - "predocs-test": "npm run docs", - "docs-test": "linkinator docs", - "fix": "gts fix", - "lint": "gts check", - "prepare": "npm run compile-protos && npm run compile", - "prefetch": "rm -rf node_modules package-lock.json google-gax*.tgz && cd ../.. && npm pack && mv google-gax*.tgz test/showcase-echo-client/google-gax.tgz", - - "system-test": "c8 mocha build/system-test", - "test": "c8 mocha build/test" + "prefetch": "rm -rf node_modules package-lock.json google-gax*.tgz gapic-tools*.tgz && cd ../.. && npm pack && mv google-gax*.tgz test/showcase-echo-client/google-gax.tgz && cd tools && npm install && npm pack && mv gapic-tools*.tgz ../test/showcase-echo-client/gapic-tools.tgz", + "prepare": "npm run compile-protos && npm run compile" }, "dependencies": { "google-gax": "./google-gax.tgz" }, "devDependencies": { - "@types/mocha": "^10.0.1", - "@types/node": "^18.11.18", - "@types/sinon": "^10.0.13", - "c8": "^7.13.0", - "gts": "^3.1.1", - "jsdoc": "^4.0.2", - "jsdoc-fresh": "^2.0.1", - "jsdoc-region-tag": "^2.0.1", - "linkinator": "^4.1.2", - "mocha": "^10.2.0", - "null-loader": "^4.0.1", - "pack-n-play": "^1.0.0-2", - "sinon": "^15.0.1", - "ts-loader": "^8.4.0", - "typescript": "^4.8.4", - "webpack": "^4.46.0", - "webpack-cli": "^4.10.0" + "@types/node": "^16.0.0", + "gapic-tools": "./gapic-tools.tgz", + "typescript": "^4.5.5" }, "engines": { "node": ">=v14" diff --git a/test/showcase-echo-client/protos/google/showcase/v1beta1/echo.proto b/test/showcase-echo-client/protos/google/showcase/v1beta1/echo.proto index 6e9d78a37..3f79b4457 100644 --- a/test/showcase-echo-client/protos/google/showcase/v1beta1/echo.proto +++ b/test/showcase-echo-client/protos/google/showcase/v1beta1/echo.proto @@ -17,6 +17,7 @@ syntax = "proto3"; import "google/api/annotations.proto"; import "google/api/client.proto"; import "google/api/field_behavior.proto"; +import "google/api/routing.proto"; import "google/longrunning/operations.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; @@ -27,27 +28,63 @@ package google.showcase.v1beta1; option go_package = "github.com/googleapis/gapic-showcase/server/genproto"; option java_package = "com.google.showcase.v1beta1"; option java_multiple_files = true; +option ruby_package = "Google::Showcase::V1beta1"; // This service is used showcase the four main types of rpcs - unary, server // side streaming, client side streaming, and bidirectional streaming. This // service also exposes methods that explicitly implement server delay, and // paginated calls. Set the 'showcase-trailer' metadata key on any method -// to have the values echoed in the response trailers. +// to have the values echoed in the response trailers. Set the +// 'x-goog-request-params' metadata key on any method to have the values +// echoed in the response headers. service Echo { // This service is meant to only run locally on the port 7469 (keypad digits // for "show"). option (google.api.default_host) = "localhost:7469"; - // This method simply echos the request. This method is showcases unary rpcs. + // This method simply echoes the request. This method showcases unary RPCs. rpc Echo(EchoRequest) returns (EchoResponse) { option (google.api.http) = { post: "/v1beta1/echo:echo" body: "*" }; + option (google.api.routing) = { + routing_parameters{ + field: "header" + } + routing_parameters{ + field: "header" + path_template: "{routing_id=**}" + } + routing_parameters{ + field: "header" + path_template: "{table_name=regions/*/zones/*/**}" + } + routing_parameters{ + field: "header" + path_template: "{super_id=projects/*}/**" + } + routing_parameters{ + field: "header" + path_template: "{table_name=projects/*/instances/*/**}" + } + routing_parameters{ + field: "header" + path_template: "projects/*/{instance_id=instances/*}/**" + } + routing_parameters{ + field: "other_header" + path_template: "{baz=**}" + } + routing_parameters{ + field: "other_header" + path_template: "{qux=projects/*}/**" + } + }; } - // This method split the given content into words and will pass each word back - // through the stream. This method showcases server-side streaming rpcs. + // This method splits the given content into words and will pass each word back + // through the stream. This method showcases server-side streaming RPCs. rpc Expand(ExpandRequest) returns (stream EchoResponse) { option (google.api.http) = { post: "/v1beta1/echo:expand" @@ -60,7 +97,7 @@ service Echo { // This method will collect the words given to it. When the stream is closed // by the client, this method will return the a concatenation of the strings - // passed to it. This method showcases client-side streaming rpcs. + // passed to it. This method showcases client-side streaming RPCs. rpc Collect(stream EchoRequest) returns (EchoResponse) { option (google.api.http) = { post: "/v1beta1/echo:collect" @@ -68,9 +105,9 @@ service Echo { }; } - // This method, upon receiving a request on the stream, the same content will - // be passed back on the stream. This method showcases bidirectional - // streaming rpcs. + // This method, upon receiving a request on the stream, will pass the same + // content back on the stream. This method showcases bidirectional + // streaming RPCs. rpc Chat(stream EchoRequest) returns (stream EchoResponse); // This is similar to the Expand method but instead of returning a stream of @@ -82,8 +119,30 @@ service Echo { }; } - // This method will wait the requested amount of and then return. - // This method showcases how a client handles a request timing out. + // This is similar to the PagedExpand except that it uses + // max_results instead of page_size, as some legacy APIs still + // do. New APIs should NOT use this pattern. + rpc PagedExpandLegacy(PagedExpandLegacyRequest) returns (PagedExpandResponse) { + option (google.api.http) = { + post: "/v1beta1/echo:pagedExpandLegacy" + body: "*" + }; + } + + // This method returns a map containing lists of words that appear in the input, keyed by their + // initial character. The only words returned are the ones included in the current page, + // as determined by page_token and page_size, which both refer to the word indices in the + // input. This paging result consisting of a map of lists is a pattern used by some legacy + // APIs. New APIs should NOT use this pattern. + rpc PagedExpandLegacyMapped(PagedExpandRequest) returns (PagedExpandLegacyMappedResponse) { + option (google.api.http) = { + post: "/v1beta1/echo:pagedExpandLegacyMapped" + body: "*" + }; + } + + // This method will wait for the requested amount of time and then return. + // This method showcases how a client handles a request timeout. rpc Wait(WaitRequest) returns (google.longrunning.Operation) { option (google.api.http) = { post: "/v1beta1/echo:wait" @@ -95,7 +154,7 @@ service Echo { }; } - // This method will block (wait) for the requested amount of time + // This method will block (wait) for the requested amount of time // and then return the response or error. // This method showcases how a client handles delays or retries. rpc Block(BlockRequest) returns (BlockResponse) { @@ -106,9 +165,19 @@ service Echo { }; } -// The request message used for the Echo, Collect and Chat methods. If content -// is set in this message then the request will succeed. If status is set in -// this message then the status will be returned as an error. +// A severity enum used to test enum capabilities in GAPIC surfaces. +enum Severity { + UNNECESSARY = 0; + NECESSARY = 1; + URGENT = 2; + CRITICAL = 3; +} + + +// The request message used for the Echo, Collect and Chat methods. +// If content or opt are set in this message then the request will succeed. +// If status is set in this message then the status will be returned as an +// error. message EchoRequest { oneof response { // The content to be echoed by the server. @@ -117,12 +186,24 @@ message EchoRequest { // The error to be thrown by the server. google.rpc.Status error = 2; } + + // The severity to be echoed by the server. + Severity severity = 3; + + // Optional. This field can be set to test the routing annotation on the Echo method. + string header = 4; + + // Optional. This field can be set to test the routing annotation on the Echo method. + string other_header = 5; } // The response message for the Echo methods. message EchoResponse { // The content specified in the request. string content = 1; + + // The severity specified in the request. + Severity severity = 2; } // The request message for the Expand method. @@ -132,6 +213,9 @@ message ExpandRequest { // The error that is thrown after all words are sent on the stream. google.rpc.Status error = 2; + + //The wait time between each server streaming messages + google.protobuf.Duration stream_wait_time = 3; } // The request for the PagedExpand method. @@ -139,13 +223,29 @@ message PagedExpandRequest { // The string to expand. string content = 1 [(google.api.field_behavior) = REQUIRED]; - // The amount of words to returned in each page. + // The number of words to returned in each page. int32 page_size = 2; // The position of the page to be returned. string page_token = 3; } +// The request for the PagedExpandLegacy method. This is a pattern used by some legacy APIs. New +// APIs should NOT use this pattern, but rather something like PagedExpandRequest which conforms to +// aip.dev/158. +message PagedExpandLegacyRequest { + // The string to expand. + string content = 1 [(google.api.field_behavior) = REQUIRED]; + + // The number of words to returned in each page. + // (-- aip.dev/not-precedent: This is a legacy, non-standard pattern that + // violates aip.dev/158. Ordinarily, this should be page_size. --) + int32 max_results = 2; + + // The position of the page to be returned. + string page_token = 3; +} + // The response for the PagedExpand method. message PagedExpandResponse { // The words that were expanded. @@ -155,6 +255,21 @@ message PagedExpandResponse { string next_page_token = 2; } +// A list of words. +message PagedExpandResponseList { + repeated string words = 1; +} + +message PagedExpandLegacyMappedResponse { + // The words that were expanded, indexed by their initial character. + // (-- aip.dev/not-precedent: This is a legacy, non-standard pattern that violates + // aip.dev/158. Ordinarily, this should be a `repeated` field, as in PagedExpandResponse. --) + map alphabetized = 1; + + // The next page token. + string next_page_token = 2; +} + // The request for Wait method. message WaitRequest { oneof end { diff --git a/test/showcase-echo-client/protos/google/showcase/v1beta1/sequence.proto b/test/showcase-echo-client/protos/google/showcase/v1beta1/sequence.proto new file mode 100644 index 000000000..4c2a6bbe0 --- /dev/null +++ b/test/showcase-echo-client/protos/google/showcase/v1beta1/sequence.proto @@ -0,0 +1,258 @@ +// Copyright 2020 Google LLC +// +// 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 +// +// https://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. + +syntax = "proto3"; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; +import "google/rpc/status.proto"; + +package google.showcase.v1beta1; + +option go_package = "github.com/googleapis/gapic-showcase/server/genproto"; +option java_package = "com.google.showcase.v1beta1"; +option java_multiple_files = true; +option ruby_package = "Google::Showcase::V1beta1"; + +service SequenceService { + // This service is meant to only run locally on the port 7469 (keypad digits + // for "show"). + option (google.api.default_host) = "localhost:7469"; + + // Creates a sequence. + rpc CreateSequence(CreateSequenceRequest) returns (Sequence) { + option (google.api.http) = { + post: "/v1beta1/sequences" + body: "sequence" + }; + option (google.api.method_signature) = "sequence"; + }; + + // Creates a sequence. + rpc CreateStreamingSequence(CreateStreamingSequenceRequest) returns (StreamingSequence) { + option (google.api.http) = { + post: "/v1beta1/streamingSequences" + body: "streaming_sequence" + }; + option (google.api.method_signature) = "streaming_sequence"; + }; + + // Retrieves a sequence. + rpc GetSequenceReport(GetSequenceReportRequest) returns (SequenceReport) { + option (google.api.http) = { + get: "/v1beta1/{name=sequences/*/sequenceReport}" + }; + option (google.api.method_signature) = "name"; + }; + + // Retrieves a sequence. + rpc GetStreamingSequenceReport(GetStreamingSequenceReportRequest) returns (StreamingSequenceReport) { + option (google.api.http) = { + get: "/v1beta1/{name=streamingSequences/*/streamingSequenceReport}" + }; + option (google.api.method_signature) = "name"; + }; + + // Attempts a sequence. + rpc AttemptSequence(AttemptSequenceRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/v1beta1/{name=sequences/*}" + body: "*" + }; + option (google.api.method_signature) = "name"; + }; + + // Attempts a streaming sequence. + rpc AttemptStreamingSequence(AttemptStreamingSequenceRequest) returns (stream AttemptStreamingSequenceResponse) { + option (google.api.http) = { + post: "/v1beta1/{name=streamingSequences/*}:stream" + body: "*" + }; + option (google.api.method_signature) = "name"; + }; +} + +message Sequence { + option (google.api.resource) = { + type: "showcase.googleapis.com/Sequence" + pattern: "sequences/{sequence}" + }; + + string name = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // A server response to an RPC Attempt in a sequence. + message Response { + // The status to return for an individual attempt. + google.rpc.Status status = 1; + + // The amount of time to delay sending the response. + google.protobuf.Duration delay = 2; + } + + // Sequence of responses to return in order for each attempt. If empty, the + // default response is an immediate OK. + repeated Response responses = 2; +} + +message StreamingSequence { + option (google.api.resource) = { + type: "showcase.googleapis.com/StreamingSequence" + pattern: "streamingSequences/{streaming_sequence}" + }; + + string name = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // The Content that the stream will send + string content = 2; + + // A server response to an RPC Attempt in a sequence. + message Response { + // The status to return for an individual attempt. + google.rpc.Status status = 1; + + // The amount of time to delay sending the response. + google.protobuf.Duration delay = 2; + + // The index that the status should be sent + int32 response_index = 3; + } + + // Sequence of responses to return in order for each attempt. If empty, the + // default response is an immediate OK. + repeated Response responses = 3; +} + + +message StreamingSequenceReport { + option (google.api.resource) = { + type: "showcase.googleapis.com/StreamingSequenceReport" + pattern: "streamingSequences/{streaming_sequence}/streamingSequenceReport" + }; + + string name = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Contains metrics on individual RPC Attempts in a sequence. + message Attempt { + // The attempt number - starting at 0. + int32 attempt_number = 1; + + // The deadline dictated by the attempt to the server. + google.protobuf.Timestamp attempt_deadline = 2; + + // The time that the server responded to the RPC attempt. Used for + // calculating attempt_delay. + google.protobuf.Timestamp response_time = 3; + + // The server perceived delay between sending the last response and + // receiving this attempt. Used for validating attempt delay backoff. + google.protobuf.Duration attempt_delay = 4; + + // The status returned to the attempt. + google.rpc.Status status = 5; + + } + + // The set of RPC attempts received by the server for a Sequence. + repeated Attempt attempts = 2; +} + +message SequenceReport { + option (google.api.resource) = { + type: "showcase.googleapis.com/SequenceReport" + pattern: "sequences/{sequence}/sequenceReport" + }; + + string name = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Contains metrics on individual RPC Attempts in a sequence. + message Attempt { + // The attempt number - starting at 0. + int32 attempt_number = 1; + + // The deadline dictated by the attempt to the server. + google.protobuf.Timestamp attempt_deadline = 2; + + // The time that the server responded to the RPC attempt. Used for + // calculating attempt_delay. + google.protobuf.Timestamp response_time = 3; + + // The server perceived delay between sending the last response and + // receiving this attempt. Used for validating attempt delay backoff. + google.protobuf.Duration attempt_delay = 4; + + // The status returned to the attempt. + google.rpc.Status status = 5; + } + + // The set of RPC attempts received by the server for a Sequence. + repeated Attempt attempts = 2; +} + +message CreateSequenceRequest { + Sequence sequence = 1; +} + +message CreateStreamingSequenceRequest { + StreamingSequence streaming_sequence = 1; +} + +message AttemptSequenceRequest { + string name = 1 [ + (google.api.resource_reference).type = "showcase.googleapis.com/Sequence", + (google.api.field_behavior) = REQUIRED + ]; + +} + +message AttemptStreamingSequenceRequest { + string name = 1 [ + (google.api.resource_reference).type = "showcase.googleapis.com/StreamingSequence", + (google.api.field_behavior) = REQUIRED + ]; + + // used to send the index of the last failed message + // in the string "content" of an AttemptStreamingSequenceResponse + // needed for stream resumption logic testing + int32 last_fail_index = 2 [ + (google.api.field_behavior) = OPTIONAL + ]; +} + +// The response message for the Echo methods. +message AttemptStreamingSequenceResponse { + // The content specified in the request. + string content = 1; + +} + +message GetSequenceReportRequest { + string name = 1 [ + (google.api.resource_reference).type = + "showcase.googleapis.com/SequenceReport", + (google.api.field_behavior) = REQUIRED + ]; +} + +message GetStreamingSequenceReportRequest { + string name = 1 [ + (google.api.resource_reference).type = + "showcase.googleapis.com/StreamingSequenceReport", + (google.api.field_behavior) = REQUIRED + ]; +} diff --git a/test/showcase-echo-client/src/index.ts b/test/showcase-echo-client/src/index.ts index cfccbdf64..3e3fba4b1 100644 --- a/test/showcase-echo-client/src/index.ts +++ b/test/showcase-echo-client/src/index.ts @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,10 +19,9 @@ import * as v1beta1 from './v1beta1'; const EchoClient = v1beta1.EchoClient; type EchoClient = v1beta1.EchoClient; -export {v1beta1, EchoClient}; -export default { - v1beta1, - EchoClient, -}; +const SequenceServiceClient = v1beta1.SequenceServiceClient; +type SequenceServiceClient = v1beta1.SequenceServiceClient; +export {v1beta1, EchoClient, SequenceServiceClient}; +export default {v1beta1, EchoClient, SequenceServiceClient}; import * as protos from '../protos/protos'; export {protos}; diff --git a/test/showcase-echo-client/src/v1beta1/echo_client.ts b/test/showcase-echo-client/src/v1beta1/echo_client.ts index 898c84887..1a481d11d 100644 --- a/test/showcase-echo-client/src/v1beta1/echo_client.ts +++ b/test/showcase-echo-client/src/v1beta1/echo_client.ts @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ // ** All changes to this file may be overwritten. ** /* global window */ -import * as gax from 'google-gax'; -import { +import type * as gax from 'google-gax'; +import type { Callback, CallOptions, Descriptors, @@ -27,16 +27,12 @@ import { LROperation, PaginationCallback, GaxCall, - GoogleError, IamClient, IamProtos, LocationsClient, LocationProtos, } from 'google-gax'; - -import {Transform} from 'stream'; -import {RequestType} from 'google-gax/build/src/apitypes'; -import {PassThrough} from 'stream'; +import {Transform, PassThrough} from 'stream'; import * as protos from '../../protos/protos'; import jsonProtos = require('../../protos/protos.json'); /** @@ -45,7 +41,6 @@ import jsonProtos = require('../../protos/protos.json'); * This file defines retry strategy and timeouts for all API methods in this library. */ import * as gapicConfig from './echo_client_config.json'; -import {operationsProtos} from 'google-gax'; const version = require('../../../package.json').version; /** @@ -53,7 +48,9 @@ const version = require('../../../package.json').version; * side streaming, client side streaming, and bidirectional streaming. This * service also exposes methods that explicitly implement server delay, and * paginated calls. Set the 'showcase-trailer' metadata key on any method - * to have the values echoed in the response trailers. + * to have the values echoed in the response trailers. Set the + * 'x-goog-request-params' metadata key on any method to have the values + * echoed in the response headers. * @class * @memberof v1beta1 */ @@ -112,8 +109,18 @@ export class EchoClient { * Pass "rest" to use HTTP/1.1 REST API instead of gRPC. * For more information, please check the * {@link https://github.com/googleapis/gax-nodejs/blob/main/client-libraries.md#http11-rest-api-mode documentation}. + * @param {gax} [gaxInstance]: loaded instance of `google-gax`. Useful if you + * need to avoid loading the default gRPC version and want to use the fallback + * HTTP implementation. Load only fallback version and pass it to the constructor: + * ``` + * const gax = require('google-gax/build/src/fallback'); // avoids loading google-gax with gRPC + * const client = new EchoClient({fallback: 'rest'}, gax); + * ``` */ - constructor(opts?: ClientOptions) { + constructor( + opts?: ClientOptions, + gaxInstance?: typeof gax | typeof gax.fallback + ) { // Ensure that options include all the required fields. const staticMembers = this.constructor as typeof EchoClient; const servicePath = @@ -133,8 +140,13 @@ export class EchoClient { opts['scopes'] = staticMembers.scopes; } + // Load google-gax module synchronously if needed + if (!gaxInstance) { + gaxInstance = require('google-gax') as typeof gax; + } + // Choose either gRPC or proto-over-HTTP implementation of google-gax. - this._gaxModule = opts.fallback ? gax.fallback : gax; + this._gaxModule = opts.fallback ? gaxInstance.fallback : gaxInstance; // Create a `gaxGrpc` object, with any grpc-specific options sent to the client. this._gaxGrpc = new this._gaxModule.GrpcClient(opts); @@ -155,9 +167,12 @@ export class EchoClient { if (servicePath === staticMembers.servicePath) { this.auth.defaultScopes = staticMembers.scopes; } - this.iamClient = new IamClient(this._gaxGrpc, opts); + this.iamClient = new this._gaxModule.IamClient(this._gaxGrpc, opts); - this.locationsClient = new LocationsClient(this._gaxGrpc, opts); + this.locationsClient = new this._gaxModule.LocationsClient( + this._gaxGrpc, + opts + ); // Determine the client header string. const clientHeader = [`gax/${this._gaxModule.version}`, `gapic/${version}`]; @@ -166,10 +181,10 @@ export class EchoClient { } else { clientHeader.push(`gl-web/${this._gaxModule.version}`); } - if (!opts.fallback) { - clientHeader.push(`grpc/${this._gaxGrpc.grpcVersion}`); - } else if (opts.fallback === 'rest') { + if (opts.fallback) { clientHeader.push(`rest/${this._gaxGrpc.grpcVersion}`); + } else { + clientHeader.push(`grpc/${this._gaxGrpc.grpcVersion}`); } if (opts.libName && opts.libVersion) { clientHeader.push(`${opts.libName}/${opts.libVersion}`); @@ -181,31 +196,18 @@ export class EchoClient { // identifiers to uniquely identify resources within the API. // Create useful helper objects for these. this.pathTemplates = { - blueprintPathTemplate: new this._gaxModule.PathTemplate( - 'sessions/{session}/tests/{test}/blueprints/{blueprint}' - ), - roomPathTemplate: new this._gaxModule.PathTemplate('rooms/{room_id}'), - roomIdBlurbIdPathTemplate: new this._gaxModule.PathTemplate( - 'rooms/{room_id}/blurbs/{blurb_id}' + sequencePathTemplate: new this._gaxModule.PathTemplate( + 'sequences/{sequence}' ), - roomIdBlurbsLegacyRoomIdBlurbIdPathTemplate: - new this._gaxModule.PathTemplate( - 'rooms/{room_id}/blurbs/legacy/{legacy_room_id}.{blurb_id}' - ), - sessionPathTemplate: new this._gaxModule.PathTemplate( - 'sessions/{session}' + sequenceReportPathTemplate: new this._gaxModule.PathTemplate( + 'sequences/{sequence}/sequenceReport' ), - testPathTemplate: new this._gaxModule.PathTemplate( - 'sessions/{session}/tests/{test}' + streamingSequencePathTemplate: new this._gaxModule.PathTemplate( + 'streamingSequences/{streaming_sequence}' ), - userPathTemplate: new this._gaxModule.PathTemplate('users/{user_id}'), - userIdProfileBlurbIdPathTemplate: new this._gaxModule.PathTemplate( - 'user/{user_id}/profile/blurbs/{blurb_id}' + streamingSequenceReportPathTemplate: new this._gaxModule.PathTemplate( + 'streamingSequences/{streaming_sequence}/streamingSequenceReport' ), - userIdProfileBlurbsLegacyUserIdBlurbIdPathTemplate: - new this._gaxModule.PathTemplate( - 'user/{user_id}/profile/blurbs/legacy/{legacy_user_id}~{blurb_id}' - ), }; // Some of the methods on this service return "paged" results, @@ -223,16 +225,19 @@ export class EchoClient { // Provide descriptors for these. this.descriptors.stream = { expand: new this._gaxModule.StreamDescriptor( - gax.StreamType.SERVER_STREAMING, - opts.fallback === 'rest' + this._gaxModule.StreamType.SERVER_STREAMING, + // legacy: opts.fallback can be a string or a boolean + opts.fallback ? true : false ), collect: new this._gaxModule.StreamDescriptor( - gax.StreamType.CLIENT_STREAMING, - opts.fallback === 'rest' + this._gaxModule.StreamType.CLIENT_STREAMING, + // legacy: opts.fallback can be a string or a boolean + opts.fallback ? true : false ), chat: new this._gaxModule.StreamDescriptor( - gax.StreamType.BIDI_STREAMING, - opts.fallback === 'rest' + this._gaxModule.StreamType.BIDI_STREAMING, + // legacy: opts.fallback can be a string or a boolean + opts.fallback ? true : false ), }; @@ -244,7 +249,7 @@ export class EchoClient { auth: this.auth, grpc: 'grpc' in this._gaxGrpc ? this._gaxGrpc.grpc : undefined, }; - if (opts.fallback === 'rest') { + if (opts.fallback) { lroOptions.protoJson = protoFilesRoot; lroOptions.httpRules = [ { @@ -343,7 +348,7 @@ export class EchoClient { this.innerApiCalls = {}; // Add a warn function to the client constructor so it can be easily tested. - this.warn = gax.warn; + this.warn = this._gaxModule.warn; } /** @@ -384,6 +389,7 @@ export class EchoClient { 'collect', 'chat', 'pagedExpand', + 'pagedExpandLegacy', 'wait', 'block', ]; @@ -397,7 +403,9 @@ export class EchoClient { setImmediate(() => { stream.emit( 'error', - new GoogleError('The client has already been closed.') + new this._gaxModule.GoogleError( + 'The client has already been closed.' + ) ); }); return stream; @@ -490,12 +498,17 @@ export class EchoClient { * The content to be echoed by the server. * @param {google.rpc.Status} request.error * The error to be thrown by the server. + * @param {google.showcase.v1beta1.Severity} request.severity + * The severity to be echoed by the server. + * @param {string} request.header + * Optional. This field can be set to test the routing annotation on the Echo method. + * @param {string} request.otherHeader + * Optional. This field can be set to test the routing annotation on the Echo method. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Promise} - The promise which resolves to an array. - * The first element of the array is an object representing [EchoResponse]{@link google.showcase.v1beta1.EchoResponse}. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods) + * The first element of the array is an object representing {@link protos.google.showcase.v1beta1.EchoResponse|EchoResponse}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } * for more details and examples. * @example include:samples/generated/v1beta1/echo.echo.js * region_tag:localhost_v1beta1_generated_Echo_Echo_async @@ -559,9 +572,202 @@ export class EchoClient { options = options || {}; options.otherArgs = options.otherArgs || {}; options.otherArgs.headers = options.otherArgs.headers || {}; + const routingParameter = {}; + { + const fieldValue = request.header; + if (fieldValue !== undefined && fieldValue !== null) { + const match = fieldValue.toString().match(RegExp('(?
.*)')); + if (match) { + const parameterValue = match.groups?.['header'] ?? fieldValue; + Object.assign(routingParameter, {header: parameterValue}); + } + } + } + { + const fieldValue = request.header; + if (fieldValue !== undefined && fieldValue !== null) { + const match = fieldValue + .toString() + .match(RegExp('(?(?:.*)?)')); + if (match) { + const parameterValue = match.groups?.['routing_id'] ?? fieldValue; + Object.assign(routingParameter, {routing_id: parameterValue}); + } + } + } + { + const fieldValue = request.header; + if (fieldValue !== undefined && fieldValue !== null) { + const match = fieldValue + .toString() + .match(RegExp('(?regions/[^/]+/zones/[^/]+(?:/.*)?)')); + if (match) { + const parameterValue = match.groups?.['table_name'] ?? fieldValue; + Object.assign(routingParameter, {table_name: parameterValue}); + } + } + } + { + const fieldValue = request.header; + if (fieldValue !== undefined && fieldValue !== null) { + const match = fieldValue + .toString() + .match(RegExp('(?projects/[^/]+)(?:/.*)?')); + if (match) { + const parameterValue = match.groups?.['super_id'] ?? fieldValue; + Object.assign(routingParameter, {super_id: parameterValue}); + } + } + } + { + const fieldValue = request.header; + if (fieldValue !== undefined && fieldValue !== null) { + const match = fieldValue + .toString() + .match( + RegExp('(?projects/[^/]+/instances/[^/]+(?:/.*)?)') + ); + if (match) { + const parameterValue = match.groups?.['table_name'] ?? fieldValue; + Object.assign(routingParameter, {table_name: parameterValue}); + } + } + } + { + const fieldValue = request.header; + if (fieldValue !== undefined && fieldValue !== null) { + const match = fieldValue + .toString() + .match( + RegExp('projects/[^/]+/(?instances/[^/]+)(?:/.*)?') + ); + if (match) { + const parameterValue = match.groups?.['instance_id'] ?? fieldValue; + Object.assign(routingParameter, {instance_id: parameterValue}); + } + } + } + { + const fieldValue = request.otherHeader; + if (fieldValue !== undefined && fieldValue !== null) { + const match = fieldValue.toString().match(RegExp('(?(?:.*)?)')); + if (match) { + const parameterValue = match.groups?.['baz'] ?? fieldValue; + Object.assign(routingParameter, {baz: parameterValue}); + } + } + } + { + const fieldValue = request.otherHeader; + if (fieldValue !== undefined && fieldValue !== null) { + const match = fieldValue + .toString() + .match(RegExp('(?projects/[^/]+)(?:/.*)?')); + if (match) { + const parameterValue = match.groups?.['qux'] ?? fieldValue; + Object.assign(routingParameter, {qux: parameterValue}); + } + } + } + options.otherArgs.headers['x-goog-request-params'] = + this._gaxModule.routingHeader.fromParams(routingParameter); this.initialize(); return this.innerApiCalls.echo(request, options, callback); } + /** + * This is similar to the PagedExpand except that it uses + * max_results instead of page_size, as some legacy APIs still + * do. New APIs should NOT use this pattern. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.content + * The string to expand. + * @param {number} request.maxResults + * The number of words to returned in each page. + * (-- aip.dev/not-precedent: This is a legacy, non-standard pattern that + * violates aip.dev/158. Ordinarily, this should be page_size. --) + * @param {string} request.pageToken + * The position of the page to be returned. + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link protos.google.showcase.v1beta1.PagedExpandResponse|PagedExpandResponse}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } + * for more details and examples. + * @example include:samples/generated/v1beta1/echo.paged_expand_legacy.js + * region_tag:localhost_v1beta1_generated_Echo_PagedExpandLegacy_async + */ + pagedExpandLegacy( + request?: protos.google.showcase.v1beta1.IPagedExpandLegacyRequest, + options?: CallOptions + ): Promise< + [ + protos.google.showcase.v1beta1.IPagedExpandResponse, + protos.google.showcase.v1beta1.IPagedExpandLegacyRequest | undefined, + {} | undefined + ] + >; + pagedExpandLegacy( + request: protos.google.showcase.v1beta1.IPagedExpandLegacyRequest, + options: CallOptions, + callback: Callback< + protos.google.showcase.v1beta1.IPagedExpandResponse, + | protos.google.showcase.v1beta1.IPagedExpandLegacyRequest + | null + | undefined, + {} | null | undefined + > + ): void; + pagedExpandLegacy( + request: protos.google.showcase.v1beta1.IPagedExpandLegacyRequest, + callback: Callback< + protos.google.showcase.v1beta1.IPagedExpandResponse, + | protos.google.showcase.v1beta1.IPagedExpandLegacyRequest + | null + | undefined, + {} | null | undefined + > + ): void; + pagedExpandLegacy( + request?: protos.google.showcase.v1beta1.IPagedExpandLegacyRequest, + optionsOrCallback?: + | CallOptions + | Callback< + protos.google.showcase.v1beta1.IPagedExpandResponse, + | protos.google.showcase.v1beta1.IPagedExpandLegacyRequest + | null + | undefined, + {} | null | undefined + >, + callback?: Callback< + protos.google.showcase.v1beta1.IPagedExpandResponse, + | protos.google.showcase.v1beta1.IPagedExpandLegacyRequest + | null + | undefined, + {} | null | undefined + > + ): Promise< + [ + protos.google.showcase.v1beta1.IPagedExpandResponse, + protos.google.showcase.v1beta1.IPagedExpandLegacyRequest | undefined, + {} | undefined + ] + > | void { + request = request || {}; + let options: CallOptions; + if (typeof optionsOrCallback === 'function' && callback === undefined) { + callback = optionsOrCallback; + options = {}; + } else { + options = optionsOrCallback as CallOptions; + } + options = options || {}; + options.otherArgs = options.otherArgs || {}; + options.otherArgs.headers = options.otherArgs.headers || {}; + this.initialize(); + return this.innerApiCalls.pagedExpandLegacy(request, options, callback); + } /** * This method will block (wait) for the requested amount of time * and then return the response or error. @@ -579,9 +785,8 @@ export class EchoClient { * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Promise} - The promise which resolves to an array. - * The first element of the array is an object representing [BlockResponse]{@link google.showcase.v1beta1.BlockResponse}. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods) + * The first element of the array is an object representing {@link protos.google.showcase.v1beta1.BlockResponse|BlockResponse}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } * for more details and examples. * @example include:samples/generated/v1beta1/echo.block.js * region_tag:localhost_v1beta1_generated_Echo_Block_async @@ -650,8 +855,8 @@ export class EchoClient { } /** - * This method split the given content into words and will pass each word back - * through the stream. This method showcases server-side streaming rpcs. + * This method splits the given content into words and will pass each word back + * through the stream. This method showcases server-side streaming RPCs. * * @param {Object} request * The request object that will be sent. @@ -659,12 +864,13 @@ export class EchoClient { * The content that will be split into words and returned on the stream. * @param {google.rpc.Status} request.error * The error that is thrown after all words are sent on the stream. + * @param {google.protobuf.Duration} request.streamWaitTime + * The wait time between each server streaming messages * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Stream} - * An object stream which emits [EchoResponse]{@link google.showcase.v1beta1.EchoResponse} on 'data' event. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#server-streaming) + * An object stream which emits {@link protos.google.showcase.v1beta1.EchoResponse|EchoResponse} on 'data' event. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#server-streaming | documentation } * for more details and examples. * @example include:samples/generated/v1beta1/echo.expand.js * region_tag:localhost_v1beta1_generated_Echo_Expand_async @@ -684,14 +890,13 @@ export class EchoClient { /** * This method will collect the words given to it. When the stream is closed * by the client, this method will return the a concatenation of the strings - * passed to it. This method showcases client-side streaming rpcs. + * passed to it. This method showcases client-side streaming RPCs. * * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Stream} - A writable stream which accepts objects representing - * [EchoRequest]{@link google.showcase.v1beta1.EchoRequest}. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#client-streaming) + * {@link protos.google.showcase.v1beta1.EchoRequest|EchoRequest}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#client-streaming | documentation } * for more details and examples. * @example include:samples/generated/v1beta1/echo.collect.js * region_tag:localhost_v1beta1_generated_Echo_Collect_async @@ -735,18 +940,17 @@ export class EchoClient { } /** - * This method, upon receiving a request on the stream, the same content will - * be passed back on the stream. This method showcases bidirectional - * streaming rpcs. + * This method, upon receiving a request on the stream, will pass the same + * content back on the stream. This method showcases bidirectional + * streaming RPCs. * * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Stream} * An object stream which is both readable and writable. It accepts objects - * representing [EchoRequest]{@link google.showcase.v1beta1.EchoRequest} for write() method, and - * will emit objects representing [EchoResponse]{@link google.showcase.v1beta1.EchoResponse} on 'data' event asynchronously. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#bi-directional-streaming) + * representing {@link protos.google.showcase.v1beta1.EchoRequest|EchoRequest} for write() method, and + * will emit objects representing {@link protos.google.showcase.v1beta1.EchoResponse|EchoResponse} on 'data' event asynchronously. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#bi-directional-streaming | documentation } * for more details and examples. * @example include:samples/generated/v1beta1/echo.chat.js * region_tag:localhost_v1beta1_generated_Echo_Chat_async @@ -757,8 +961,8 @@ export class EchoClient { } /** - * This method will wait the requested amount of and then return. - * This method showcases how a client handles a request timing out. + * This method will wait for the requested amount of time and then return. + * This method showcases how a client handles a request timeout. * * @param {Object} request * The request object that will be sent. @@ -777,8 +981,7 @@ export class EchoClient { * The first element of the array is an object representing * a long running operation. Its `promise()` method returns a promise * you can `await` for. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#long-running-operations) + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#long-running-operations | documentation } * for more details and examples. * @example include:samples/generated/v1beta1/echo.wait.js * region_tag:localhost_v1beta1_generated_Echo_Wait_async @@ -869,8 +1072,7 @@ export class EchoClient { * The operation name that will be passed. * @returns {Promise} - The promise which resolves to an object. * The decoded operation object has result and metadata field to get information from. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#long-running-operations) + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#long-running-operations | documentation } * for more details and examples. * @example include:samples/generated/v1beta1/echo.wait.js * region_tag:localhost_v1beta1_generated_Echo_Wait_async @@ -883,11 +1085,12 @@ export class EchoClient { protos.google.showcase.v1beta1.WaitMetadata > > { - const request = new operationsProtos.google.longrunning.GetOperationRequest( - {name} - ); + const request = + new this._gaxModule.operationsProtos.google.longrunning.GetOperationRequest( + {name} + ); const [operation] = await this.operationsClient.getOperation(request); - const decodeOperation = new gax.Operation( + const decodeOperation = new this._gaxModule.Operation( operation, this.descriptors.longrunning.wait, this._gaxModule.createDefaultBackoffSettings() @@ -906,20 +1109,19 @@ export class EchoClient { * @param {string} request.content * The string to expand. * @param {number} request.pageSize - * The amount of words to returned in each page. + * The number of words to returned in each page. * @param {string} request.pageToken * The position of the page to be returned. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Promise} - The promise which resolves to an array. - * The first element of the array is Array of [EchoResponse]{@link google.showcase.v1beta1.EchoResponse}. + * The first element of the array is Array of {@link protos.google.showcase.v1beta1.EchoResponse|EchoResponse}. * The client library will perform auto-pagination by default: it will call the API as many * times as needed and will merge results from all the pages into this array. * Note that it can affect your quota. * We recommend using `pagedExpandAsync()` * method described below for async iteration which you can stop as needed. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination) + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination | documentation } * for more details and examples. */ pagedExpand( @@ -994,19 +1196,18 @@ export class EchoClient { * @param {string} request.content * The string to expand. * @param {number} request.pageSize - * The amount of words to returned in each page. + * The number of words to returned in each page. * @param {string} request.pageToken * The position of the page to be returned. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Stream} - * An object stream which emits an object representing [EchoResponse]{@link google.showcase.v1beta1.EchoResponse} on 'data' event. + * An object stream which emits an object representing {@link protos.google.showcase.v1beta1.EchoResponse|EchoResponse} on 'data' event. * The client library will perform auto-pagination by default: it will call the API as many * times as needed. Note that it can affect your quota. * We recommend using `pagedExpandAsync()` * method described below for async iteration which you can stop as needed. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination) + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination | documentation } * for more details and examples. */ pagedExpandStream( @@ -1021,7 +1222,7 @@ export class EchoClient { const callSettings = defaultCallSettings.merge(options); this.initialize(); return this.descriptors.page.pagedExpand.createStream( - this.innerApiCalls.pagedExpand as gax.GaxCall, + this.innerApiCalls.pagedExpand as GaxCall, request, callSettings ); @@ -1036,18 +1237,17 @@ export class EchoClient { * @param {string} request.content * The string to expand. * @param {number} request.pageSize - * The amount of words to returned in each page. + * The number of words to returned in each page. * @param {string} request.pageToken * The position of the page to be returned. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Object} - * An iterable Object that allows [async iteration](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols). + * An iterable Object that allows {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols | async iteration }. * When you iterate the returned iterable, each element will be an object representing - * [EchoResponse]{@link google.showcase.v1beta1.EchoResponse}. The API will be called under the hood as needed, once per the page, + * {@link protos.google.showcase.v1beta1.EchoResponse|EchoResponse}. The API will be called under the hood as needed, once per the page, * so you can stop the iteration when you don't need more results. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination) + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination | documentation } * for more details and examples. * @example include:samples/generated/v1beta1/echo.paged_expand.js * region_tag:localhost_v1beta1_generated_Echo_PagedExpand_async @@ -1065,7 +1265,7 @@ export class EchoClient { this.initialize(); return this.descriptors.page.pagedExpand.asyncIterate( this.innerApiCalls['pagedExpand'] as GaxCall, - request as unknown as RequestType, + request as {}, callSettings ) as AsyncIterable; } @@ -1082,16 +1282,16 @@ export class EchoClient { * OPTIONAL: A `GetPolicyOptions` object for specifying options to * `GetIamPolicy`. This field is only used by Cloud IAM. * - * This object should have the same structure as [GetPolicyOptions]{@link google.iam.v1.GetPolicyOptions} + * This object should have the same structure as {@link google.iam.v1.GetPolicyOptions | GetPolicyOptions}. * @param {Object} [options] * Optional parameters. You can override the default settings for this call, e.g, timeout, - * retries, paginations, etc. See [gax.CallOptions]{@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html} for the details. + * retries, paginations, etc. See {@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html | gax.CallOptions} for the details. * @param {function(?Error, ?Object)} [callback] * The function which will be called with the result of the API call. * - * The second parameter to the callback is an object representing [Policy]{@link google.iam.v1.Policy}. + * The second parameter to the callback is an object representing {@link google.iam.v1.Policy | Policy}. * @returns {Promise} - The promise which resolves to an array. - * The first element of the array is an object representing [Policy]{@link google.iam.v1.Policy}. + * The first element of the array is an object representing {@link google.iam.v1.Policy | Policy}. * The promise has a method named "cancel" which cancels the ongoing API call. */ getIamPolicy( @@ -1108,7 +1308,7 @@ export class EchoClient { IamProtos.google.iam.v1.GetIamPolicyRequest | null | undefined, {} | null | undefined > - ): Promise { + ): Promise<[IamProtos.google.iam.v1.Policy]> { return this.iamClient.getIamPolicy(request, options, callback); } @@ -1129,17 +1329,16 @@ export class EchoClient { * @param {string[]} request.permissions * The set of permissions to check for the `resource`. Permissions with * wildcards (such as '*' or 'storage.*') are not allowed. For more - * information see - * [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions). + * information see {@link https://cloud.google.com/iam/docs/overview#permissions | IAM Overview }. * @param {Object} [options] * Optional parameters. You can override the default settings for this call, e.g, timeout, - * retries, paginations, etc. See [gax.CallOptions]{@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html} for the details. + * retries, paginations, etc. See {@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html | gax.CallOptions} for the details. * @param {function(?Error, ?Object)} [callback] * The function which will be called with the result of the API call. * - * The second parameter to the callback is an object representing [TestIamPermissionsResponse]{@link google.iam.v1.TestIamPermissionsResponse}. + * The second parameter to the callback is an object representing {@link google.iam.v1.TestIamPermissionsResponse | TestIamPermissionsResponse}. * @returns {Promise} - The promise which resolves to an array. - * The first element of the array is an object representing [TestIamPermissionsResponse]{@link google.iam.v1.TestIamPermissionsResponse}. + * The first element of the array is an object representing {@link google.iam.v1.TestIamPermissionsResponse | TestIamPermissionsResponse}. * The promise has a method named "cancel" which cancels the ongoing API call. */ setIamPolicy( @@ -1156,7 +1355,7 @@ export class EchoClient { IamProtos.google.iam.v1.SetIamPolicyRequest | null | undefined, {} | null | undefined > - ): Promise { + ): Promise<[IamProtos.google.iam.v1.Policy]> { return this.iamClient.setIamPolicy(request, options, callback); } @@ -1177,17 +1376,16 @@ export class EchoClient { * @param {string[]} request.permissions * The set of permissions to check for the `resource`. Permissions with * wildcards (such as '*' or 'storage.*') are not allowed. For more - * information see - * [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions). + * information see {@link https://cloud.google.com/iam/docs/overview#permissions | IAM Overview }. * @param {Object} [options] * Optional parameters. You can override the default settings for this call, e.g, timeout, - * retries, paginations, etc. See [gax.CallOptions]{@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html} for the details. + * retries, paginations, etc. See {@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html | gax.CallOptions} for the details. * @param {function(?Error, ?Object)} [callback] * The function which will be called with the result of the API call. * - * The second parameter to the callback is an object representing [TestIamPermissionsResponse]{@link google.iam.v1.TestIamPermissionsResponse}. + * The second parameter to the callback is an object representing {@link google.iam.v1.TestIamPermissionsResponse | TestIamPermissionsResponse}. * @returns {Promise} - The promise which resolves to an array. - * The first element of the array is an object representing [TestIamPermissionsResponse]{@link google.iam.v1.TestIamPermissionsResponse}. + * The first element of the array is an object representing {@link google.iam.v1.TestIamPermissionsResponse | TestIamPermissionsResponse}. * The promise has a method named "cancel" which cancels the ongoing API call. * */ @@ -1205,7 +1403,7 @@ export class EchoClient { IamProtos.google.iam.v1.TestIamPermissionsRequest | null | undefined, {} | null | undefined > - ): Promise { + ): Promise<[IamProtos.google.iam.v1.TestIamPermissionsResponse]> { return this.iamClient.testIamPermissions(request, options, callback); } @@ -1217,11 +1415,10 @@ export class EchoClient { * @param {string} request.name * Resource name for the location. * @param {object} [options] - * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html | CallOptions} for more details. * @returns {Promise} - The promise which resolves to an array. - * The first element of the array is an object representing [Location]{@link google.cloud.location.Location}. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods) + * The first element of the array is an object representing {@link google.cloud.location.Location | Location}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } * for more details and examples. * @example * ``` @@ -1267,12 +1464,11 @@ export class EchoClient { * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Object} - * An iterable Object that allows [async iteration](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols). + * An iterable Object that allows {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols | async iteration }. * When you iterate the returned iterable, each element will be an object representing - * [Location]{@link google.cloud.location.Location}. The API will be called under the hood as needed, once per the page, + * {@link google.cloud.location.Location | Location}. The API will be called under the hood as needed, once per the page, * so you can stop the iteration when you don't need more results. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination) + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination | documentation } * for more details and examples. * @example * ``` @@ -1298,20 +1494,18 @@ export class EchoClient { * @param {string} request.name - The name of the operation resource. * @param {Object=} options * Optional parameters. You can override the default settings for this call, - * e.g, timeout, retries, paginations, etc. See [gax.CallOptions]{@link - * https://googleapis.github.io/gax-nodejs/global.html#CallOptions} for the - * details. + * e.g, timeout, retries, paginations, etc. See {@link + * https://googleapis.github.io/gax-nodejs/global.html#CallOptions | gax.CallOptions} + * for the details. * @param {function(?Error, ?Object)=} callback * The function which will be called with the result of the API call. * * The second parameter to the callback is an object representing - * [google.longrunning.Operation]{@link - * external:"google.longrunning.Operation"}. + * {@link google.longrunning.Operation | google.longrunning.Operation}. * @return {Promise} - The promise which resolves to an array. * The first element of the array is an object representing - * [google.longrunning.Operation]{@link - * external:"google.longrunning.Operation"}. The promise has a method named - * "cancel" which cancels the ongoing API call. + * {@link google.longrunning.Operation | google.longrunning.Operation}. + * The promise has a method named "cancel" which cancels the ongoing API call. * * @example * ``` @@ -1355,11 +1549,11 @@ export class EchoClient { * resources in a page. * @param {Object=} options * Optional parameters. You can override the default settings for this call, - * e.g, timeout, retries, paginations, etc. See [gax.CallOptions]{@link - * https://googleapis.github.io/gax-nodejs/global.html#CallOptions} for the + * e.g, timeout, retries, paginations, etc. See {@link + * https://googleapis.github.io/gax-nodejs/global.html#CallOptions | gax.CallOptions} for the * details. * @returns {Object} - * An iterable Object that conforms to @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols. + * An iterable Object that conforms to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols | iteration protocols}. * * @example * ``` @@ -1390,8 +1584,8 @@ export class EchoClient { * @param {string} request.name - The name of the operation resource to be cancelled. * @param {Object=} options * Optional parameters. You can override the default settings for this call, - * e.g, timeout, retries, paginations, etc. See [gax.CallOptions]{@link - * https://googleapis.github.io/gax-nodejs/global.html#CallOptions} for the + * e.g, timeout, retries, paginations, etc. See {@link + * https://googleapis.github.io/gax-nodejs/global.html#CallOptions | gax.CallOptions} for the * details. * @param {function(?Error)=} callback * The function which will be called with the result of the API call. @@ -1433,9 +1627,9 @@ export class EchoClient { * @param {string} request.name - The name of the operation resource to be deleted. * @param {Object=} options * Optional parameters. You can override the default settings for this call, - * e.g, timeout, retries, paginations, etc. See [gax.CallOptions]{@link - * https://googleapis.github.io/gax-nodejs/global.html#CallOptions} for the - * details. + * e.g, timeout, retries, paginations, etc. See {@link + * https://googleapis.github.io/gax-nodejs/global.html#CallOptions | gax.CallOptions} + * for the details. * @param {function(?Error)=} callback * The function which will be called with the result of the API call. * @return {Promise} - The promise which resolves when API call finishes. @@ -1471,371 +1665,105 @@ export class EchoClient { // -------------------- /** - * Return a fully-qualified blueprint resource name string. + * Return a fully-qualified sequence resource name string. * - * @param {string} session - * @param {string} test - * @param {string} blueprint + * @param {string} sequence * @returns {string} Resource name string. */ - blueprintPath(session: string, test: string, blueprint: string) { - return this.pathTemplates.blueprintPathTemplate.render({ - session: session, - test: test, - blueprint: blueprint, + sequencePath(sequence: string) { + return this.pathTemplates.sequencePathTemplate.render({ + sequence: sequence, }); } /** - * Parse the session from Blueprint resource. + * Parse the sequence from Sequence resource. * - * @param {string} blueprintName - * A fully-qualified path representing Blueprint resource. - * @returns {string} A string representing the session. + * @param {string} sequenceName + * A fully-qualified path representing Sequence resource. + * @returns {string} A string representing the sequence. */ - matchSessionFromBlueprintName(blueprintName: string) { - return this.pathTemplates.blueprintPathTemplate.match(blueprintName) - .session; + matchSequenceFromSequenceName(sequenceName: string) { + return this.pathTemplates.sequencePathTemplate.match(sequenceName).sequence; } /** - * Parse the test from Blueprint resource. + * Return a fully-qualified sequenceReport resource name string. * - * @param {string} blueprintName - * A fully-qualified path representing Blueprint resource. - * @returns {string} A string representing the test. - */ - matchTestFromBlueprintName(blueprintName: string) { - return this.pathTemplates.blueprintPathTemplate.match(blueprintName).test; - } - - /** - * Parse the blueprint from Blueprint resource. - * - * @param {string} blueprintName - * A fully-qualified path representing Blueprint resource. - * @returns {string} A string representing the blueprint. - */ - matchBlueprintFromBlueprintName(blueprintName: string) { - return this.pathTemplates.blueprintPathTemplate.match(blueprintName) - .blueprint; - } - - /** - * Return a fully-qualified room resource name string. - * - * @param {string} room_id + * @param {string} sequence * @returns {string} Resource name string. */ - roomPath(roomId: string) { - return this.pathTemplates.roomPathTemplate.render({ - room_id: roomId, + sequenceReportPath(sequence: string) { + return this.pathTemplates.sequenceReportPathTemplate.render({ + sequence: sequence, }); } /** - * Parse the room_id from Room resource. + * Parse the sequence from SequenceReport resource. * - * @param {string} roomName - * A fully-qualified path representing Room resource. - * @returns {string} A string representing the room_id. + * @param {string} sequenceReportName + * A fully-qualified path representing SequenceReport resource. + * @returns {string} A string representing the sequence. */ - matchRoomIdFromRoomName(roomName: string) { - return this.pathTemplates.roomPathTemplate.match(roomName).room_id; + matchSequenceFromSequenceReportName(sequenceReportName: string) { + return this.pathTemplates.sequenceReportPathTemplate.match( + sequenceReportName + ).sequence; } /** - * Return a fully-qualified roomIdBlurbId resource name string. + * Return a fully-qualified streamingSequence resource name string. * - * @param {string} room_id - * @param {string} blurb_id + * @param {string} streaming_sequence * @returns {string} Resource name string. */ - roomIdBlurbIdPath(roomId: string, blurbId: string) { - return this.pathTemplates.roomIdBlurbIdPathTemplate.render({ - room_id: roomId, - blurb_id: blurbId, + streamingSequencePath(streamingSequence: string) { + return this.pathTemplates.streamingSequencePathTemplate.render({ + streaming_sequence: streamingSequence, }); } /** - * Parse the room_id from RoomIdBlurbId resource. - * - * @param {string} roomIdBlurbIdName - * A fully-qualified path representing room_id_blurb_id resource. - * @returns {string} A string representing the room_id. - */ - matchRoomIdFromRoomIdBlurbIdName(roomIdBlurbIdName: string) { - return this.pathTemplates.roomIdBlurbIdPathTemplate.match(roomIdBlurbIdName) - .room_id; - } - - /** - * Parse the blurb_id from RoomIdBlurbId resource. + * Parse the streaming_sequence from StreamingSequence resource. * - * @param {string} roomIdBlurbIdName - * A fully-qualified path representing room_id_blurb_id resource. - * @returns {string} A string representing the blurb_id. - */ - matchBlurbIdFromRoomIdBlurbIdName(roomIdBlurbIdName: string) { - return this.pathTemplates.roomIdBlurbIdPathTemplate.match(roomIdBlurbIdName) - .blurb_id; - } - - /** - * Return a fully-qualified roomIdBlurbsLegacyRoomIdBlurbId resource name string. - * - * @param {string} room_id - * @param {string} legacy_room_id - * @param {string} blurb_id - * @returns {string} Resource name string. + * @param {string} streamingSequenceName + * A fully-qualified path representing StreamingSequence resource. + * @returns {string} A string representing the streaming_sequence. */ - roomIdBlurbsLegacyRoomIdBlurbIdPath( - roomId: string, - legacyRoomId: string, - blurbId: string + matchStreamingSequenceFromStreamingSequenceName( + streamingSequenceName: string ) { - return this.pathTemplates.roomIdBlurbsLegacyRoomIdBlurbIdPathTemplate.render( - { - room_id: roomId, - legacy_room_id: legacyRoomId, - blurb_id: blurbId, - } - ); - } - - /** - * Parse the room_id from RoomIdBlurbsLegacyRoomIdBlurbId resource. - * - * @param {string} roomIdBlurbsLegacyRoomIdBlurbIdName - * A fully-qualified path representing room_id_blurbs_legacy_room_id_blurb_id resource. - * @returns {string} A string representing the room_id. - */ - matchRoomIdFromRoomIdBlurbsLegacyRoomIdBlurbIdName( - roomIdBlurbsLegacyRoomIdBlurbIdName: string - ) { - return this.pathTemplates.roomIdBlurbsLegacyRoomIdBlurbIdPathTemplate.match( - roomIdBlurbsLegacyRoomIdBlurbIdName - ).room_id; - } - - /** - * Parse the legacy_room_id from RoomIdBlurbsLegacyRoomIdBlurbId resource. - * - * @param {string} roomIdBlurbsLegacyRoomIdBlurbIdName - * A fully-qualified path representing room_id_blurbs_legacy_room_id_blurb_id resource. - * @returns {string} A string representing the legacy_room_id. - */ - matchLegacyRoomIdFromRoomIdBlurbsLegacyRoomIdBlurbIdName( - roomIdBlurbsLegacyRoomIdBlurbIdName: string - ) { - return this.pathTemplates.roomIdBlurbsLegacyRoomIdBlurbIdPathTemplate.match( - roomIdBlurbsLegacyRoomIdBlurbIdName - ).legacy_room_id; - } - - /** - * Parse the blurb_id from RoomIdBlurbsLegacyRoomIdBlurbId resource. - * - * @param {string} roomIdBlurbsLegacyRoomIdBlurbIdName - * A fully-qualified path representing room_id_blurbs_legacy_room_id_blurb_id resource. - * @returns {string} A string representing the blurb_id. - */ - matchBlurbIdFromRoomIdBlurbsLegacyRoomIdBlurbIdName( - roomIdBlurbsLegacyRoomIdBlurbIdName: string - ) { - return this.pathTemplates.roomIdBlurbsLegacyRoomIdBlurbIdPathTemplate.match( - roomIdBlurbsLegacyRoomIdBlurbIdName - ).blurb_id; - } - - /** - * Return a fully-qualified session resource name string. - * - * @param {string} session - * @returns {string} Resource name string. - */ - sessionPath(session: string) { - return this.pathTemplates.sessionPathTemplate.render({ - session: session, - }); - } - - /** - * Parse the session from Session resource. - * - * @param {string} sessionName - * A fully-qualified path representing Session resource. - * @returns {string} A string representing the session. - */ - matchSessionFromSessionName(sessionName: string) { - return this.pathTemplates.sessionPathTemplate.match(sessionName).session; - } - - /** - * Return a fully-qualified test resource name string. - * - * @param {string} session - * @param {string} test - * @returns {string} Resource name string. - */ - testPath(session: string, test: string) { - return this.pathTemplates.testPathTemplate.render({ - session: session, - test: test, - }); + return this.pathTemplates.streamingSequencePathTemplate.match( + streamingSequenceName + ).streaming_sequence; } /** - * Parse the session from Test resource. + * Return a fully-qualified streamingSequenceReport resource name string. * - * @param {string} testName - * A fully-qualified path representing Test resource. - * @returns {string} A string representing the session. - */ - matchSessionFromTestName(testName: string) { - return this.pathTemplates.testPathTemplate.match(testName).session; - } - - /** - * Parse the test from Test resource. - * - * @param {string} testName - * A fully-qualified path representing Test resource. - * @returns {string} A string representing the test. - */ - matchTestFromTestName(testName: string) { - return this.pathTemplates.testPathTemplate.match(testName).test; - } - - /** - * Return a fully-qualified user resource name string. - * - * @param {string} user_id + * @param {string} streaming_sequence * @returns {string} Resource name string. */ - userPath(userId: string) { - return this.pathTemplates.userPathTemplate.render({ - user_id: userId, + streamingSequenceReportPath(streamingSequence: string) { + return this.pathTemplates.streamingSequenceReportPathTemplate.render({ + streaming_sequence: streamingSequence, }); } /** - * Parse the user_id from User resource. - * - * @param {string} userName - * A fully-qualified path representing User resource. - * @returns {string} A string representing the user_id. - */ - matchUserIdFromUserName(userName: string) { - return this.pathTemplates.userPathTemplate.match(userName).user_id; - } - - /** - * Return a fully-qualified userIdProfileBlurbId resource name string. - * - * @param {string} user_id - * @param {string} blurb_id - * @returns {string} Resource name string. - */ - userIdProfileBlurbIdPath(userId: string, blurbId: string) { - return this.pathTemplates.userIdProfileBlurbIdPathTemplate.render({ - user_id: userId, - blurb_id: blurbId, - }); - } - - /** - * Parse the user_id from UserIdProfileBlurbId resource. - * - * @param {string} userIdProfileBlurbIdName - * A fully-qualified path representing user_id_profile_blurb_id resource. - * @returns {string} A string representing the user_id. - */ - matchUserIdFromUserIdProfileBlurbIdName(userIdProfileBlurbIdName: string) { - return this.pathTemplates.userIdProfileBlurbIdPathTemplate.match( - userIdProfileBlurbIdName - ).user_id; - } - - /** - * Parse the blurb_id from UserIdProfileBlurbId resource. - * - * @param {string} userIdProfileBlurbIdName - * A fully-qualified path representing user_id_profile_blurb_id resource. - * @returns {string} A string representing the blurb_id. - */ - matchBlurbIdFromUserIdProfileBlurbIdName(userIdProfileBlurbIdName: string) { - return this.pathTemplates.userIdProfileBlurbIdPathTemplate.match( - userIdProfileBlurbIdName - ).blurb_id; - } - - /** - * Return a fully-qualified userIdProfileBlurbsLegacyUserIdBlurbId resource name string. - * - * @param {string} user_id - * @param {string} legacy_user_id - * @param {string} blurb_id - * @returns {string} Resource name string. - */ - userIdProfileBlurbsLegacyUserIdBlurbIdPath( - userId: string, - legacyUserId: string, - blurbId: string - ) { - return this.pathTemplates.userIdProfileBlurbsLegacyUserIdBlurbIdPathTemplate.render( - { - user_id: userId, - legacy_user_id: legacyUserId, - blurb_id: blurbId, - } - ); - } - - /** - * Parse the user_id from UserIdProfileBlurbsLegacyUserIdBlurbId resource. - * - * @param {string} userIdProfileBlurbsLegacyUserIdBlurbIdName - * A fully-qualified path representing user_id_profile_blurbs_legacy_user_id_blurb_id resource. - * @returns {string} A string representing the user_id. - */ - matchUserIdFromUserIdProfileBlurbsLegacyUserIdBlurbIdName( - userIdProfileBlurbsLegacyUserIdBlurbIdName: string - ) { - return this.pathTemplates.userIdProfileBlurbsLegacyUserIdBlurbIdPathTemplate.match( - userIdProfileBlurbsLegacyUserIdBlurbIdName - ).user_id; - } - - /** - * Parse the legacy_user_id from UserIdProfileBlurbsLegacyUserIdBlurbId resource. - * - * @param {string} userIdProfileBlurbsLegacyUserIdBlurbIdName - * A fully-qualified path representing user_id_profile_blurbs_legacy_user_id_blurb_id resource. - * @returns {string} A string representing the legacy_user_id. - */ - matchLegacyUserIdFromUserIdProfileBlurbsLegacyUserIdBlurbIdName( - userIdProfileBlurbsLegacyUserIdBlurbIdName: string - ) { - return this.pathTemplates.userIdProfileBlurbsLegacyUserIdBlurbIdPathTemplate.match( - userIdProfileBlurbsLegacyUserIdBlurbIdName - ).legacy_user_id; - } - - /** - * Parse the blurb_id from UserIdProfileBlurbsLegacyUserIdBlurbId resource. + * Parse the streaming_sequence from StreamingSequenceReport resource. * - * @param {string} userIdProfileBlurbsLegacyUserIdBlurbIdName - * A fully-qualified path representing user_id_profile_blurbs_legacy_user_id_blurb_id resource. - * @returns {string} A string representing the blurb_id. + * @param {string} streamingSequenceReportName + * A fully-qualified path representing StreamingSequenceReport resource. + * @returns {string} A string representing the streaming_sequence. */ - matchBlurbIdFromUserIdProfileBlurbsLegacyUserIdBlurbIdName( - userIdProfileBlurbsLegacyUserIdBlurbIdName: string + matchStreamingSequenceFromStreamingSequenceReportName( + streamingSequenceReportName: string ) { - return this.pathTemplates.userIdProfileBlurbsLegacyUserIdBlurbIdPathTemplate.match( - userIdProfileBlurbsLegacyUserIdBlurbIdName - ).blurb_id; + return this.pathTemplates.streamingSequenceReportPathTemplate.match( + streamingSequenceReportName + ).streaming_sequence; } /** diff --git a/test/showcase-echo-client/src/v1beta1/echo_client_config.json b/test/showcase-echo-client/src/v1beta1/echo_client_config.json index c30d10733..57bd509e9 100644 --- a/test/showcase-echo-client/src/v1beta1/echo_client_config.json +++ b/test/showcase-echo-client/src/v1beta1/echo_client_config.json @@ -40,6 +40,14 @@ "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, + "PagedExpandLegacy": { + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "PagedExpandLegacyMapped": { + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, "Wait": { "retry_codes_name": "non_idempotent", "retry_params_name": "default" diff --git a/test/showcase-echo-client/src/v1beta1/echo_proto_list.json b/test/showcase-echo-client/src/v1beta1/echo_proto_list.json index 4d911013e..76b3da61a 100644 --- a/test/showcase-echo-client/src/v1beta1/echo_proto_list.json +++ b/test/showcase-echo-client/src/v1beta1/echo_proto_list.json @@ -1,3 +1,4 @@ [ - "../../protos/google/showcase/v1beta1/echo.proto" + "../../protos/google/showcase/v1beta1/echo.proto", + "../../protos/google/showcase/v1beta1/sequence.proto" ] diff --git a/test/showcase-echo-client/src/v1beta1/index.ts b/test/showcase-echo-client/src/v1beta1/index.ts index eb1faebc8..be6bb2377 100644 --- a/test/showcase-echo-client/src/v1beta1/index.ts +++ b/test/showcase-echo-client/src/v1beta1/index.ts @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,3 +17,4 @@ // ** All changes to this file may be overwritten. ** export {EchoClient} from './echo_client'; +export {SequenceServiceClient} from './sequence_service_client'; diff --git a/test/showcase-echo-client/src/v1beta1/sequence_service_client.ts b/test/showcase-echo-client/src/v1beta1/sequence_service_client.ts new file mode 100644 index 000000000..1149d230a --- /dev/null +++ b/test/showcase-echo-client/src/v1beta1/sequence_service_client.ts @@ -0,0 +1,1182 @@ +// Copyright 2023 Google LLC +// +// 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 +// +// https://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. +// +// ** This file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +/* global window */ +import type * as gax from 'google-gax'; +import type { + Callback, + CallOptions, + Descriptors, + ClientOptions, + IamClient, + IamProtos, + LocationsClient, + LocationProtos, +} from 'google-gax'; +import {PassThrough} from 'stream'; +import * as protos from '../../protos/protos'; +import jsonProtos = require('../../protos/protos.json'); +/** + * Client JSON configuration object, loaded from + * `src/v1beta1/sequence_service_client_config.json`. + * This file defines retry strategy and timeouts for all API methods in this library. + */ +import * as gapicConfig from './sequence_service_client_config.json'; +const version = require('../../../package.json').version; + +/** + * @class + * @memberof v1beta1 + */ +export class SequenceServiceClient { + private _terminated = false; + private _opts: ClientOptions; + private _providedCustomServicePath: boolean; + private _gaxModule: typeof gax | typeof gax.fallback; + private _gaxGrpc: gax.GrpcClient | gax.fallback.GrpcClient; + private _protos: {}; + private _defaults: {[method: string]: gax.CallSettings}; + auth: gax.GoogleAuth; + descriptors: Descriptors = { + page: {}, + stream: {}, + longrunning: {}, + batching: {}, + }; + warn: (code: string, message: string, warnType?: string) => void; + innerApiCalls: {[name: string]: Function}; + iamClient: IamClient; + locationsClient: LocationsClient; + pathTemplates: {[name: string]: gax.PathTemplate}; + sequenceServiceStub?: Promise<{[name: string]: Function}>; + + /** + * Construct an instance of SequenceServiceClient. + * + * @param {object} [options] - The configuration object. + * The options accepted by the constructor are described in detail + * in [this document](https://github.com/googleapis/gax-nodejs/blob/main/client-libraries.md#creating-the-client-instance). + * The common options are: + * @param {object} [options.credentials] - Credentials object. + * @param {string} [options.credentials.client_email] + * @param {string} [options.credentials.private_key] + * @param {string} [options.email] - Account email address. Required when + * using a .pem or .p12 keyFilename. + * @param {string} [options.keyFilename] - Full path to the a .json, .pem, or + * .p12 key downloaded from the Google Developers Console. If you provide + * a path to a JSON file, the projectId option below is not necessary. + * NOTE: .pem and .p12 require you to specify options.email as well. + * @param {number} [options.port] - The port on which to connect to + * the remote host. + * @param {string} [options.projectId] - The project ID from the Google + * Developer's Console, e.g. 'grape-spaceship-123'. We will also check + * the environment variable GCLOUD_PROJECT for your project ID. If your + * app is running in an environment which supports + * {@link https://developers.google.com/identity/protocols/application-default-credentials Application Default Credentials}, + * your project ID will be detected automatically. + * @param {string} [options.apiEndpoint] - The domain name of the + * API remote host. + * @param {gax.ClientConfig} [options.clientConfig] - Client configuration override. + * Follows the structure of {@link gapicConfig}. + * @param {boolean | "rest"} [options.fallback] - Use HTTP fallback mode. + * Pass "rest" to use HTTP/1.1 REST API instead of gRPC. + * For more information, please check the + * {@link https://github.com/googleapis/gax-nodejs/blob/main/client-libraries.md#http11-rest-api-mode documentation}. + * @param {gax} [gaxInstance]: loaded instance of `google-gax`. Useful if you + * need to avoid loading the default gRPC version and want to use the fallback + * HTTP implementation. Load only fallback version and pass it to the constructor: + * ``` + * const gax = require('google-gax/build/src/fallback'); // avoids loading google-gax with gRPC + * const client = new SequenceServiceClient({fallback: 'rest'}, gax); + * ``` + */ + constructor( + opts?: ClientOptions, + gaxInstance?: typeof gax | typeof gax.fallback + ) { + // Ensure that options include all the required fields. + const staticMembers = this.constructor as typeof SequenceServiceClient; + const servicePath = + opts?.servicePath || opts?.apiEndpoint || staticMembers.servicePath; + this._providedCustomServicePath = !!( + opts?.servicePath || opts?.apiEndpoint + ); + const port = opts?.port || staticMembers.port; + const clientConfig = opts?.clientConfig ?? {}; + const fallback = + opts?.fallback ?? + (typeof window !== 'undefined' && typeof window?.fetch === 'function'); + opts = Object.assign({servicePath, port, clientConfig, fallback}, opts); + + // If scopes are unset in options and we're connecting to a non-default endpoint, set scopes just in case. + if (servicePath !== staticMembers.servicePath && !('scopes' in opts)) { + opts['scopes'] = staticMembers.scopes; + } + + // Load google-gax module synchronously if needed + if (!gaxInstance) { + gaxInstance = require('google-gax') as typeof gax; + } + + // Choose either gRPC or proto-over-HTTP implementation of google-gax. + this._gaxModule = opts.fallback ? gaxInstance.fallback : gaxInstance; + + // Create a `gaxGrpc` object, with any grpc-specific options sent to the client. + this._gaxGrpc = new this._gaxModule.GrpcClient(opts); + + // Save options to use in initialize() method. + this._opts = opts; + + // Save the auth object to the client, for use by other methods. + this.auth = this._gaxGrpc.auth as gax.GoogleAuth; + + // Set useJWTAccessWithScope on the auth object. + this.auth.useJWTAccessWithScope = true; + + // Set defaultServicePath on the auth object. + this.auth.defaultServicePath = staticMembers.servicePath; + + // Set the default scopes in auth client if needed. + if (servicePath === staticMembers.servicePath) { + this.auth.defaultScopes = staticMembers.scopes; + } + this.iamClient = new this._gaxModule.IamClient(this._gaxGrpc, opts); + + this.locationsClient = new this._gaxModule.LocationsClient( + this._gaxGrpc, + opts + ); + + // Determine the client header string. + const clientHeader = [`gax/${this._gaxModule.version}`, `gapic/${version}`]; + if (typeof process !== 'undefined' && 'versions' in process) { + clientHeader.push(`gl-node/${process.versions.node}`); + } else { + clientHeader.push(`gl-web/${this._gaxModule.version}`); + } + if (!opts.fallback) { + clientHeader.push(`grpc/${this._gaxGrpc.grpcVersion}`); + } else if (opts.fallback === 'rest') { + clientHeader.push(`rest/${this._gaxGrpc.grpcVersion}`); + } + if (opts.libName && opts.libVersion) { + clientHeader.push(`${opts.libName}/${opts.libVersion}`); + } + // Load the applicable protos. + this._protos = this._gaxGrpc.loadProtoJSON(jsonProtos); + + // This API contains "path templates"; forward-slash-separated + // identifiers to uniquely identify resources within the API. + // Create useful helper objects for these. + this.pathTemplates = { + sequencePathTemplate: new this._gaxModule.PathTemplate( + 'sequences/{sequence}' + ), + sequenceReportPathTemplate: new this._gaxModule.PathTemplate( + 'sequences/{sequence}/sequenceReport' + ), + streamingSequencePathTemplate: new this._gaxModule.PathTemplate( + 'streamingSequences/{streaming_sequence}' + ), + streamingSequenceReportPathTemplate: new this._gaxModule.PathTemplate( + 'streamingSequences/{streaming_sequence}/streamingSequenceReport' + ), + }; + + // Some of the methods on this service provide streaming responses. + // Provide descriptors for these. + this.descriptors.stream = { + attemptStreamingSequence: new this._gaxModule.StreamDescriptor( + this._gaxModule.StreamType.SERVER_STREAMING, + opts.fallback === 'rest', + this._opts.newRetry + ), + }; + + // Put together the default options sent with requests. + this._defaults = this._gaxGrpc.constructSettings( + 'google.showcase.v1beta1.SequenceService', + gapicConfig as gax.ClientConfig, + opts.clientConfig || {}, + {'x-goog-api-client': clientHeader.join(' ')} + ); + + // Set up a dictionary of "inner API calls"; the core implementation + // of calling the API is handled in `google-gax`, with this code + // merely providing the destination and request information. + this.innerApiCalls = {}; + + // Add a warn function to the client constructor so it can be easily tested. + this.warn = this._gaxModule.warn; + } + + /** + * Initialize the client. + * Performs asynchronous operations (such as authentication) and prepares the client. + * This function will be called automatically when any class method is called for the + * first time, but if you need to initialize it before calling an actual method, + * feel free to call initialize() directly. + * + * You can await on this method if you want to make sure the client is initialized. + * + * @returns {Promise} A promise that resolves to an authenticated service stub. + */ + initialize() { + // If the client stub promise is already initialized, return immediately. + if (this.sequenceServiceStub) { + return this.sequenceServiceStub; + } + + // Put together the "service stub" for + // google.showcase.v1beta1.SequenceService. + this.sequenceServiceStub = this._gaxGrpc.createStub( + this._opts.fallback + ? (this._protos as protobuf.Root).lookupService( + 'google.showcase.v1beta1.SequenceService' + ) + : // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this._protos as any).google.showcase.v1beta1.SequenceService, + this._opts, + this._providedCustomServicePath + ) as Promise<{[method: string]: Function}>; + + // Iterate over each of the methods that the service provides + // and create an API call method for each. + const sequenceServiceStubMethods = [ + 'createSequence', + 'createStreamingSequence', + 'getSequenceReport', + 'getStreamingSequenceReport', + 'attemptSequence', + 'attemptStreamingSequence', + ]; + for (const methodName of sequenceServiceStubMethods) { + const callPromise = this.sequenceServiceStub.then( + stub => + (...args: Array<{}>) => { + if (this._terminated) { + if (methodName in this.descriptors.stream) { + const stream = new PassThrough(); + setImmediate(() => { + stream.emit( + 'error', + new this._gaxModule.GoogleError( + 'The client has already been closed.' + ) + ); + }); + return stream; + } + return Promise.reject('The client has already been closed.'); + } + const func = stub[methodName]; + return func.apply(stub, args); + }, + (err: Error | null | undefined) => () => { + throw err; + } + ); + + const descriptor = this.descriptors.stream[methodName] || undefined; + const apiCall = this._gaxModule.createApiCall( + callPromise, + this._defaults[methodName], + descriptor, + this._opts.fallback + ); + + this.innerApiCalls[methodName] = apiCall; + } + + return this.sequenceServiceStub; + } + + /** + * The DNS address for this API service. + * @returns {string} The DNS address for this service. + */ + static get servicePath() { + return 'localhost'; + } + + /** + * The DNS address for this API service - same as servicePath(), + * exists for compatibility reasons. + * @returns {string} The DNS address for this service. + */ + static get apiEndpoint() { + return 'localhost'; + } + + /** + * The port for this API service. + * @returns {number} The default port for this service. + */ + static get port() { + return 7469; + } + + /** + * The scopes needed to make gRPC calls for every method defined + * in this service. + * @returns {string[]} List of default scopes. + */ + static get scopes() { + return []; + } + + getProjectId(): Promise; + getProjectId(callback: Callback): void; + /** + * Return the project ID used by this class. + * @returns {Promise} A promise that resolves to string containing the project ID. + */ + getProjectId( + callback?: Callback + ): Promise | void { + if (callback) { + this.auth.getProjectId(callback); + return; + } + return this.auth.getProjectId(); + } + + // ------------------- + // -- Service calls -- + // ------------------- + /** + * Creates a sequence. + * + * @param {Object} request + * The request object that will be sent. + * @param {google.showcase.v1beta1.Sequence} request.sequence + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link protos.google.showcase.v1beta1.Sequence|Sequence}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } + * for more details and examples. + * @example include:samples/generated/v1beta1/sequence_service.create_sequence.js + * region_tag:localhost_v1beta1_generated_SequenceService_CreateSequence_async + */ + createSequence( + request?: protos.google.showcase.v1beta1.ICreateSequenceRequest, + options?: CallOptions + ): Promise< + [ + protos.google.showcase.v1beta1.ISequence, + protos.google.showcase.v1beta1.ICreateSequenceRequest | undefined, + {} | undefined + ] + >; + createSequence( + request: protos.google.showcase.v1beta1.ICreateSequenceRequest, + options: CallOptions, + callback: Callback< + protos.google.showcase.v1beta1.ISequence, + protos.google.showcase.v1beta1.ICreateSequenceRequest | null | undefined, + {} | null | undefined + > + ): void; + createSequence( + request: protos.google.showcase.v1beta1.ICreateSequenceRequest, + callback: Callback< + protos.google.showcase.v1beta1.ISequence, + protos.google.showcase.v1beta1.ICreateSequenceRequest | null | undefined, + {} | null | undefined + > + ): void; + createSequence( + request?: protos.google.showcase.v1beta1.ICreateSequenceRequest, + optionsOrCallback?: + | CallOptions + | Callback< + protos.google.showcase.v1beta1.ISequence, + | protos.google.showcase.v1beta1.ICreateSequenceRequest + | null + | undefined, + {} | null | undefined + >, + callback?: Callback< + protos.google.showcase.v1beta1.ISequence, + protos.google.showcase.v1beta1.ICreateSequenceRequest | null | undefined, + {} | null | undefined + > + ): Promise< + [ + protos.google.showcase.v1beta1.ISequence, + protos.google.showcase.v1beta1.ICreateSequenceRequest | undefined, + {} | undefined + ] + > | void { + request = request || {}; + let options: CallOptions; + if (typeof optionsOrCallback === 'function' && callback === undefined) { + callback = optionsOrCallback; + options = {}; + } else { + options = optionsOrCallback as CallOptions; + } + options = options || {}; + options.otherArgs = options.otherArgs || {}; + options.otherArgs.headers = options.otherArgs.headers || {}; + this.initialize(); + return this.innerApiCalls.createSequence(request, options, callback); + } + /** + * Creates a sequence. + * + * @param {Object} request + * The request object that will be sent. + * @param {google.showcase.v1beta1.StreamingSequence} request.streamingSequence + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link protos.google.showcase.v1beta1.StreamingSequence|StreamingSequence}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } + * for more details and examples. + * @example include:samples/generated/v1beta1/sequence_service.create_streaming_sequence.js + * region_tag:localhost_v1beta1_generated_SequenceService_CreateStreamingSequence_async + */ + createStreamingSequence( + request?: protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest, + options?: CallOptions + ): Promise< + [ + protos.google.showcase.v1beta1.IStreamingSequence, + ( + | protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest + | undefined + ), + {} | undefined + ] + >; + createStreamingSequence( + request: protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest, + options: CallOptions, + callback: Callback< + protos.google.showcase.v1beta1.IStreamingSequence, + | protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest + | null + | undefined, + {} | null | undefined + > + ): void; + createStreamingSequence( + request: protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest, + callback: Callback< + protos.google.showcase.v1beta1.IStreamingSequence, + | protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest + | null + | undefined, + {} | null | undefined + > + ): void; + createStreamingSequence( + request?: protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest, + optionsOrCallback?: + | CallOptions + | Callback< + protos.google.showcase.v1beta1.IStreamingSequence, + | protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest + | null + | undefined, + {} | null | undefined + >, + callback?: Callback< + protos.google.showcase.v1beta1.IStreamingSequence, + | protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest + | null + | undefined, + {} | null | undefined + > + ): Promise< + [ + protos.google.showcase.v1beta1.IStreamingSequence, + ( + | protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest + | undefined + ), + {} | undefined + ] + > | void { + request = request || {}; + let options: CallOptions; + if (typeof optionsOrCallback === 'function' && callback === undefined) { + callback = optionsOrCallback; + options = {}; + } else { + options = optionsOrCallback as CallOptions; + } + options = options || {}; + options.otherArgs = options.otherArgs || {}; + options.otherArgs.headers = options.otherArgs.headers || {}; + this.initialize(); + return this.innerApiCalls.createStreamingSequence( + request, + options, + callback + ); + } + /** + * Retrieves a sequence. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.name + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link protos.google.showcase.v1beta1.SequenceReport|SequenceReport}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } + * for more details and examples. + * @example include:samples/generated/v1beta1/sequence_service.get_sequence_report.js + * region_tag:localhost_v1beta1_generated_SequenceService_GetSequenceReport_async + */ + getSequenceReport( + request?: protos.google.showcase.v1beta1.IGetSequenceReportRequest, + options?: CallOptions + ): Promise< + [ + protos.google.showcase.v1beta1.ISequenceReport, + protos.google.showcase.v1beta1.IGetSequenceReportRequest | undefined, + {} | undefined + ] + >; + getSequenceReport( + request: protos.google.showcase.v1beta1.IGetSequenceReportRequest, + options: CallOptions, + callback: Callback< + protos.google.showcase.v1beta1.ISequenceReport, + | protos.google.showcase.v1beta1.IGetSequenceReportRequest + | null + | undefined, + {} | null | undefined + > + ): void; + getSequenceReport( + request: protos.google.showcase.v1beta1.IGetSequenceReportRequest, + callback: Callback< + protos.google.showcase.v1beta1.ISequenceReport, + | protos.google.showcase.v1beta1.IGetSequenceReportRequest + | null + | undefined, + {} | null | undefined + > + ): void; + getSequenceReport( + request?: protos.google.showcase.v1beta1.IGetSequenceReportRequest, + optionsOrCallback?: + | CallOptions + | Callback< + protos.google.showcase.v1beta1.ISequenceReport, + | protos.google.showcase.v1beta1.IGetSequenceReportRequest + | null + | undefined, + {} | null | undefined + >, + callback?: Callback< + protos.google.showcase.v1beta1.ISequenceReport, + | protos.google.showcase.v1beta1.IGetSequenceReportRequest + | null + | undefined, + {} | null | undefined + > + ): Promise< + [ + protos.google.showcase.v1beta1.ISequenceReport, + protos.google.showcase.v1beta1.IGetSequenceReportRequest | undefined, + {} | undefined + ] + > | void { + request = request || {}; + let options: CallOptions; + if (typeof optionsOrCallback === 'function' && callback === undefined) { + callback = optionsOrCallback; + options = {}; + } else { + options = optionsOrCallback as CallOptions; + } + options = options || {}; + options.otherArgs = options.otherArgs || {}; + options.otherArgs.headers = options.otherArgs.headers || {}; + options.otherArgs.headers['x-goog-request-params'] = + this._gaxModule.routingHeader.fromParams({ + name: request.name ?? '', + }); + this.initialize(); + return this.innerApiCalls.getSequenceReport(request, options, callback); + } + /** + * Retrieves a sequence. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.name + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link protos.google.showcase.v1beta1.StreamingSequenceReport|StreamingSequenceReport}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } + * for more details and examples. + * @example include:samples/generated/v1beta1/sequence_service.get_streaming_sequence_report.js + * region_tag:localhost_v1beta1_generated_SequenceService_GetStreamingSequenceReport_async + */ + getStreamingSequenceReport( + request?: protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest, + options?: CallOptions + ): Promise< + [ + protos.google.showcase.v1beta1.IStreamingSequenceReport, + ( + | protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest + | undefined + ), + {} | undefined + ] + >; + getStreamingSequenceReport( + request: protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest, + options: CallOptions, + callback: Callback< + protos.google.showcase.v1beta1.IStreamingSequenceReport, + | protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest + | null + | undefined, + {} | null | undefined + > + ): void; + getStreamingSequenceReport( + request: protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest, + callback: Callback< + protos.google.showcase.v1beta1.IStreamingSequenceReport, + | protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest + | null + | undefined, + {} | null | undefined + > + ): void; + getStreamingSequenceReport( + request?: protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest, + optionsOrCallback?: + | CallOptions + | Callback< + protos.google.showcase.v1beta1.IStreamingSequenceReport, + | protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest + | null + | undefined, + {} | null | undefined + >, + callback?: Callback< + protos.google.showcase.v1beta1.IStreamingSequenceReport, + | protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest + | null + | undefined, + {} | null | undefined + > + ): Promise< + [ + protos.google.showcase.v1beta1.IStreamingSequenceReport, + ( + | protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest + | undefined + ), + {} | undefined + ] + > | void { + request = request || {}; + let options: CallOptions; + if (typeof optionsOrCallback === 'function' && callback === undefined) { + callback = optionsOrCallback; + options = {}; + } else { + options = optionsOrCallback as CallOptions; + } + options = options || {}; + options.otherArgs = options.otherArgs || {}; + options.otherArgs.headers = options.otherArgs.headers || {}; + options.otherArgs.headers['x-goog-request-params'] = + this._gaxModule.routingHeader.fromParams({ + name: request.name ?? '', + }); + this.initialize(); + return this.innerApiCalls.getStreamingSequenceReport( + request, + options, + callback + ); + } + /** + * Attempts a sequence. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.name + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link protos.google.protobuf.Empty|Empty}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } + * for more details and examples. + * @example include:samples/generated/v1beta1/sequence_service.attempt_sequence.js + * region_tag:localhost_v1beta1_generated_SequenceService_AttemptSequence_async + */ + attemptSequence( + request?: protos.google.showcase.v1beta1.IAttemptSequenceRequest, + options?: CallOptions + ): Promise< + [ + protos.google.protobuf.IEmpty, + protos.google.showcase.v1beta1.IAttemptSequenceRequest | undefined, + {} | undefined + ] + >; + attemptSequence( + request: protos.google.showcase.v1beta1.IAttemptSequenceRequest, + options: CallOptions, + callback: Callback< + protos.google.protobuf.IEmpty, + protos.google.showcase.v1beta1.IAttemptSequenceRequest | null | undefined, + {} | null | undefined + > + ): void; + attemptSequence( + request: protos.google.showcase.v1beta1.IAttemptSequenceRequest, + callback: Callback< + protos.google.protobuf.IEmpty, + protos.google.showcase.v1beta1.IAttemptSequenceRequest | null | undefined, + {} | null | undefined + > + ): void; + attemptSequence( + request?: protos.google.showcase.v1beta1.IAttemptSequenceRequest, + optionsOrCallback?: + | CallOptions + | Callback< + protos.google.protobuf.IEmpty, + | protos.google.showcase.v1beta1.IAttemptSequenceRequest + | null + | undefined, + {} | null | undefined + >, + callback?: Callback< + protos.google.protobuf.IEmpty, + protos.google.showcase.v1beta1.IAttemptSequenceRequest | null | undefined, + {} | null | undefined + > + ): Promise< + [ + protos.google.protobuf.IEmpty, + protos.google.showcase.v1beta1.IAttemptSequenceRequest | undefined, + {} | undefined + ] + > | void { + request = request || {}; + let options: CallOptions; + if (typeof optionsOrCallback === 'function' && callback === undefined) { + callback = optionsOrCallback; + options = {}; + } else { + options = optionsOrCallback as CallOptions; + } + options = options || {}; + options.otherArgs = options.otherArgs || {}; + options.otherArgs.headers = options.otherArgs.headers || {}; + options.otherArgs.headers['x-goog-request-params'] = + this._gaxModule.routingHeader.fromParams({ + name: request.name ?? '', + }); + this.initialize(); + return this.innerApiCalls.attemptSequence(request, options, callback); + } + + /** + * Attempts a streaming sequence. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.name + * @param {number} [request.lastFailIndex] + * used to send the index of the last failed message + * in the string "content" of an AttemptStreamingSequenceResponse + * needed for stream resumption logic testing + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * @returns {Stream} + * An object stream which emits {@link protos.google.showcase.v1beta1.AttemptStreamingSequenceResponse|AttemptStreamingSequenceResponse} on 'data' event. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#server-streaming | documentation } + * for more details and examples. + * @example include:samples/generated/v1beta1/sequence_service.attempt_streaming_sequence.js + * region_tag:localhost_v1beta1_generated_SequenceService_AttemptStreamingSequence_async + */ + attemptStreamingSequence( + request?: protos.google.showcase.v1beta1.IAttemptStreamingSequenceRequest, + options?: CallOptions + ): gax.CancellableStream { + request = request || {}; + options = options || {}; + options.otherArgs = options.otherArgs || {}; + options.otherArgs.headers = options.otherArgs.headers || {}; + options.otherArgs.headers['x-goog-request-params'] = + this._gaxModule.routingHeader.fromParams({ + name: request.name ?? '', + }); + this.initialize(); + return this.innerApiCalls.attemptStreamingSequence(request, options); + } + + /** + * Gets the access control policy for a resource. Returns an empty policy + * if the resource exists and does not have a policy set. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.resource + * REQUIRED: The resource for which the policy is being requested. + * See the operation documentation for the appropriate value for this field. + * @param {Object} [request.options] + * OPTIONAL: A `GetPolicyOptions` object for specifying options to + * `GetIamPolicy`. This field is only used by Cloud IAM. + * + * This object should have the same structure as {@link google.iam.v1.GetPolicyOptions | GetPolicyOptions}. + * @param {Object} [options] + * Optional parameters. You can override the default settings for this call, e.g, timeout, + * retries, paginations, etc. See {@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html | gax.CallOptions} for the details. + * @param {function(?Error, ?Object)} [callback] + * The function which will be called with the result of the API call. + * + * The second parameter to the callback is an object representing {@link google.iam.v1.Policy | Policy}. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link google.iam.v1.Policy | Policy}. + * The promise has a method named "cancel" which cancels the ongoing API call. + */ + getIamPolicy( + request: IamProtos.google.iam.v1.GetIamPolicyRequest, + options?: + | gax.CallOptions + | Callback< + IamProtos.google.iam.v1.Policy, + IamProtos.google.iam.v1.GetIamPolicyRequest | null | undefined, + {} | null | undefined + >, + callback?: Callback< + IamProtos.google.iam.v1.Policy, + IamProtos.google.iam.v1.GetIamPolicyRequest | null | undefined, + {} | null | undefined + > + ): Promise<[IamProtos.google.iam.v1.Policy]> { + return this.iamClient.getIamPolicy(request, options, callback); + } + + /** + * Returns permissions that a caller has on the specified resource. If the + * resource does not exist, this will return an empty set of + * permissions, not a NOT_FOUND error. + * + * Note: This operation is designed to be used for building + * permission-aware UIs and command-line tools, not for authorization + * checking. This operation may "fail open" without warning. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.resource + * REQUIRED: The resource for which the policy detail is being requested. + * See the operation documentation for the appropriate value for this field. + * @param {string[]} request.permissions + * The set of permissions to check for the `resource`. Permissions with + * wildcards (such as '*' or 'storage.*') are not allowed. For more + * information see {@link https://cloud.google.com/iam/docs/overview#permissions | IAM Overview }. + * @param {Object} [options] + * Optional parameters. You can override the default settings for this call, e.g, timeout, + * retries, paginations, etc. See {@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html | gax.CallOptions} for the details. + * @param {function(?Error, ?Object)} [callback] + * The function which will be called with the result of the API call. + * + * The second parameter to the callback is an object representing {@link google.iam.v1.TestIamPermissionsResponse | TestIamPermissionsResponse}. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link google.iam.v1.TestIamPermissionsResponse | TestIamPermissionsResponse}. + * The promise has a method named "cancel" which cancels the ongoing API call. + */ + setIamPolicy( + request: IamProtos.google.iam.v1.SetIamPolicyRequest, + options?: + | gax.CallOptions + | Callback< + IamProtos.google.iam.v1.Policy, + IamProtos.google.iam.v1.SetIamPolicyRequest | null | undefined, + {} | null | undefined + >, + callback?: Callback< + IamProtos.google.iam.v1.Policy, + IamProtos.google.iam.v1.SetIamPolicyRequest | null | undefined, + {} | null | undefined + > + ): Promise<[IamProtos.google.iam.v1.Policy]> { + return this.iamClient.setIamPolicy(request, options, callback); + } + + /** + * Returns permissions that a caller has on the specified resource. If the + * resource does not exist, this will return an empty set of + * permissions, not a NOT_FOUND error. + * + * Note: This operation is designed to be used for building + * permission-aware UIs and command-line tools, not for authorization + * checking. This operation may "fail open" without warning. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.resource + * REQUIRED: The resource for which the policy detail is being requested. + * See the operation documentation for the appropriate value for this field. + * @param {string[]} request.permissions + * The set of permissions to check for the `resource`. Permissions with + * wildcards (such as '*' or 'storage.*') are not allowed. For more + * information see {@link https://cloud.google.com/iam/docs/overview#permissions | IAM Overview }. + * @param {Object} [options] + * Optional parameters. You can override the default settings for this call, e.g, timeout, + * retries, paginations, etc. See {@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html | gax.CallOptions} for the details. + * @param {function(?Error, ?Object)} [callback] + * The function which will be called with the result of the API call. + * + * The second parameter to the callback is an object representing {@link google.iam.v1.TestIamPermissionsResponse | TestIamPermissionsResponse}. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link google.iam.v1.TestIamPermissionsResponse | TestIamPermissionsResponse}. + * The promise has a method named "cancel" which cancels the ongoing API call. + * + */ + testIamPermissions( + request: IamProtos.google.iam.v1.TestIamPermissionsRequest, + options?: + | gax.CallOptions + | Callback< + IamProtos.google.iam.v1.TestIamPermissionsResponse, + IamProtos.google.iam.v1.TestIamPermissionsRequest | null | undefined, + {} | null | undefined + >, + callback?: Callback< + IamProtos.google.iam.v1.TestIamPermissionsResponse, + IamProtos.google.iam.v1.TestIamPermissionsRequest | null | undefined, + {} | null | undefined + > + ): Promise<[IamProtos.google.iam.v1.TestIamPermissionsResponse]> { + return this.iamClient.testIamPermissions(request, options, callback); + } + + /** + * Gets information about a location. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.name + * Resource name for the location. + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html | CallOptions} for more details. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link google.cloud.location.Location | Location}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } + * for more details and examples. + * @example + * ``` + * const [response] = await client.getLocation(request); + * ``` + */ + getLocation( + request: LocationProtos.google.cloud.location.IGetLocationRequest, + options?: + | gax.CallOptions + | Callback< + LocationProtos.google.cloud.location.ILocation, + | LocationProtos.google.cloud.location.IGetLocationRequest + | null + | undefined, + {} | null | undefined + >, + callback?: Callback< + LocationProtos.google.cloud.location.ILocation, + | LocationProtos.google.cloud.location.IGetLocationRequest + | null + | undefined, + {} | null | undefined + > + ): Promise { + return this.locationsClient.getLocation(request, options, callback); + } + + /** + * Lists information about the supported locations for this service. Returns an iterable object. + * + * `for`-`await`-`of` syntax is used with the iterable to get response elements on-demand. + * @param {Object} request + * The request object that will be sent. + * @param {string} request.name + * The resource that owns the locations collection, if applicable. + * @param {string} request.filter + * The standard list filter. + * @param {number} request.pageSize + * The standard list page size. + * @param {string} request.pageToken + * The standard list page token. + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * @returns {Object} + * An iterable Object that allows {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols | async iteration }. + * When you iterate the returned iterable, each element will be an object representing + * {@link google.cloud.location.Location | Location}. The API will be called under the hood as needed, once per the page, + * so you can stop the iteration when you don't need more results. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination | documentation } + * for more details and examples. + * @example + * ``` + * const iterable = client.listLocationsAsync(request); + * for await (const response of iterable) { + * // process response + * } + * ``` + */ + listLocationsAsync( + request: LocationProtos.google.cloud.location.IListLocationsRequest, + options?: CallOptions + ): AsyncIterable { + return this.locationsClient.listLocationsAsync(request, options); + } + + // -------------------- + // -- Path templates -- + // -------------------- + + /** + * Return a fully-qualified sequence resource name string. + * + * @param {string} sequence + * @returns {string} Resource name string. + */ + sequencePath(sequence: string) { + return this.pathTemplates.sequencePathTemplate.render({ + sequence: sequence, + }); + } + + /** + * Parse the sequence from Sequence resource. + * + * @param {string} sequenceName + * A fully-qualified path representing Sequence resource. + * @returns {string} A string representing the sequence. + */ + matchSequenceFromSequenceName(sequenceName: string) { + return this.pathTemplates.sequencePathTemplate.match(sequenceName).sequence; + } + + /** + * Return a fully-qualified sequenceReport resource name string. + * + * @param {string} sequence + * @returns {string} Resource name string. + */ + sequenceReportPath(sequence: string) { + return this.pathTemplates.sequenceReportPathTemplate.render({ + sequence: sequence, + }); + } + + /** + * Parse the sequence from SequenceReport resource. + * + * @param {string} sequenceReportName + * A fully-qualified path representing SequenceReport resource. + * @returns {string} A string representing the sequence. + */ + matchSequenceFromSequenceReportName(sequenceReportName: string) { + return this.pathTemplates.sequenceReportPathTemplate.match( + sequenceReportName + ).sequence; + } + + /** + * Return a fully-qualified streamingSequence resource name string. + * + * @param {string} streaming_sequence + * @returns {string} Resource name string. + */ + streamingSequencePath(streamingSequence: string) { + return this.pathTemplates.streamingSequencePathTemplate.render({ + streaming_sequence: streamingSequence, + }); + } + + /** + * Parse the streaming_sequence from StreamingSequence resource. + * + * @param {string} streamingSequenceName + * A fully-qualified path representing StreamingSequence resource. + * @returns {string} A string representing the streaming_sequence. + */ + matchStreamingSequenceFromStreamingSequenceName( + streamingSequenceName: string + ) { + return this.pathTemplates.streamingSequencePathTemplate.match( + streamingSequenceName + ).streaming_sequence; + } + + /** + * Return a fully-qualified streamingSequenceReport resource name string. + * + * @param {string} streaming_sequence + * @returns {string} Resource name string. + */ + streamingSequenceReportPath(streamingSequence: string) { + return this.pathTemplates.streamingSequenceReportPathTemplate.render({ + streaming_sequence: streamingSequence, + }); + } + + /** + * Parse the streaming_sequence from StreamingSequenceReport resource. + * + * @param {string} streamingSequenceReportName + * A fully-qualified path representing StreamingSequenceReport resource. + * @returns {string} A string representing the streaming_sequence. + */ + matchStreamingSequenceFromStreamingSequenceReportName( + streamingSequenceReportName: string + ) { + return this.pathTemplates.streamingSequenceReportPathTemplate.match( + streamingSequenceReportName + ).streaming_sequence; + } + + /** + * Terminate the gRPC channel and close the client. + * + * The client will no longer be usable and all future behavior is undefined. + * @returns {Promise} A promise that resolves when the client is closed. + */ + close(): Promise { + if (this.sequenceServiceStub && !this._terminated) { + return this.sequenceServiceStub.then(stub => { + this._terminated = true; + stub.close(); + this.iamClient.close(); + this.locationsClient.close(); + }); + } + return Promise.resolve(); + } +} diff --git a/test/showcase-echo-client/src/v1beta1/sequence_service_client_config.json b/test/showcase-echo-client/src/v1beta1/sequence_service_client_config.json new file mode 100644 index 000000000..955c6fcec --- /dev/null +++ b/test/showcase-echo-client/src/v1beta1/sequence_service_client_config.json @@ -0,0 +1,50 @@ +{ + "interfaces": { + "google.showcase.v1beta1.SequenceService": { + "retry_codes": { + "non_idempotent": [], + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ] + }, + "retry_params": { + "default": { + "initial_retry_delay_millis": 100, + "retry_delay_multiplier": 1.3, + "max_retry_delay_millis": 60000, + "initial_rpc_timeout_millis": 60000, + "rpc_timeout_multiplier": 1, + "max_rpc_timeout_millis": 60000, + "total_timeout_millis": 600000 + } + }, + "methods": { + "CreateSequence": { + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "CreateStreamingSequence": { + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "GetSequenceReport": { + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "GetStreamingSequenceReport": { + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "AttemptSequence": { + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "AttemptStreamingSequence": { + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + } + } + } + } +} diff --git a/test/showcase-echo-client/src/v1beta1/sequence_service_proto_list.json b/test/showcase-echo-client/src/v1beta1/sequence_service_proto_list.json new file mode 100644 index 000000000..76b3da61a --- /dev/null +++ b/test/showcase-echo-client/src/v1beta1/sequence_service_proto_list.json @@ -0,0 +1,4 @@ +[ + "../../protos/google/showcase/v1beta1/echo.proto", + "../../protos/google/showcase/v1beta1/sequence.proto" +] diff --git a/test/showcase-server/package.json b/test/showcase-server/package.json index 0568a8651..f8d4dbfe5 100644 --- a/test/showcase-server/package.json +++ b/test/showcase-server/package.json @@ -19,7 +19,7 @@ "dependencies": { "download": "^8.0.0", "execa": "^5.0.0", - "rimraf": "^3.0.0" + "rimraf": "^5.0.1" }, "devDependencies": { "@types/download": "^8.0.1", diff --git a/test/showcase-server/src/index.ts b/test/showcase-server/src/index.ts index c67405814..bf1ff6ab7 100644 --- a/test/showcase-server/src/index.ts +++ b/test/showcase-server/src/index.ts @@ -18,11 +18,11 @@ import * as execa from 'execa'; import * as download from 'download'; import * as fs from 'fs'; import * as path from 'path'; -import * as rimraf from 'rimraf'; +import {rimraf} from 'rimraf'; import * as util from 'util'; const mkdir = util.promisify(fs.mkdir); -const rmrf = util.promisify(rimraf); + const timeout = 5000; // wait after the server launches function sleep(timeoutMs: number) { @@ -36,12 +36,12 @@ export class ShowcaseServer { const testDir = path.join(process.cwd(), '.showcase-server-dir'); const platform = process.platform; const arch = process.arch === 'x64' ? 'amd64' : process.arch; - const showcaseVersion = process.env['SHOWCASE_VERSION'] || '0.23.0'; + const showcaseVersion = process.env['SHOWCASE_VERSION'] || '0.28.4'; const tarballFilename = `gapic-showcase-${showcaseVersion}-${platform}-${arch}.tar.gz`; const fallbackServerUrl = `https://github.com/googleapis/gapic-showcase/releases/download/v${showcaseVersion}/${tarballFilename}`; const binaryName = './gapic-showcase'; - await rmrf(testDir); + await rimraf(testDir); await mkdir(testDir); process.chdir(testDir); console.log(`Server will be run from ${testDir}.`); diff --git a/test/system-test/test.clientlibs.ts b/test/system-test/test.clientlibs.ts index efe822b80..9391df900 100644 --- a/test/system-test/test.clientlibs.ts +++ b/test/system-test/test.clientlibs.ts @@ -17,11 +17,10 @@ import * as execa from 'execa'; import * as fs from 'fs'; import * as path from 'path'; -import * as rimraf from 'rimraf'; +import {rimraf} from 'rimraf'; import * as util from 'util'; import {describe, it, before} from 'mocha'; -const rmrf = util.promisify(rimraf); const mkdir = util.promisify(fs.mkdir); const readFile = util.promisify(fs.readFile); const writeFile = util.promisify(fs.writeFile); @@ -38,6 +37,9 @@ const gaxDir = path.resolve(__dirname, '..', '..', '..'); // eslint-disable-next-line @typescript-eslint/no-var-requires const pkg = require('../../../package.json'); const gaxTarball = path.join(gaxDir, `${pkg.name}-${pkg.version}.tgz`); +const toolsPkg = require('../../../tools/package.json'); +const toolsBasename = `${toolsPkg.name}-${toolsPkg.version}.tgz`; +const toolsTarball = path.join(gaxDir, toolsBasename); async function latestRelease( cwd: string, @@ -121,8 +123,7 @@ async function preparePackage( const packageJsonStr = (await readFile(packageJson)).toString(); const packageJsonObj = JSON.parse(packageJsonStr); packageJsonObj['dependencies']['google-gax'] = `file:${gaxTarball}`; - await writeFile(packageJson, JSON.stringify(packageJsonObj, null, ' ')); - packageJsonObj['dependencies']['google-gax'] = `file:${gaxTarball}`; + packageJsonObj['devDependencies']['gapic-tools'] = `file:${toolsTarball}`; await writeFile(packageJson, JSON.stringify(packageJsonObj, null, ' ')); await execa('npm', ['install'], { cwd: inMonorepo ? packagePath : packageName, @@ -176,25 +177,40 @@ describe('Run system tests for some libraries', () => { before(async () => { console.log('Packing google-gax...'); await execa('npm', ['pack'], {cwd: gaxDir, stdio: 'inherit'}); - if (!fs.existsSync(gaxTarball)) { throw new Error(`npm pack tarball ${gaxTarball} does not exist`); } + console.log('Packing gapic-tools...'); + await execa('npm', ['install'], { + cwd: path.join(gaxDir, 'tools'), + stdio: 'inherit', + }); + await execa('npm', ['pack'], { + cwd: path.join(gaxDir, 'tools'), + stdio: 'inherit', + }); + await fs.promises.rename( + path.join(gaxDir, 'tools', toolsBasename), + toolsTarball + ); + if (!fs.existsSync(toolsTarball)) { + throw new Error(`npm pack tarball ${toolsTarball} does not exist`); + } - await rmrf(testDir); + await rimraf(testDir); await mkdir(testDir); process.chdir(testDir); console.log(`Running tests in ${testDir}.`); }); // Speech has unary, LRO, and streaming - // Speech is not in the monorepo + // Speech is in the google-cloud-node monorepo describe('speech', () => { before(async () => { - await preparePackage('nodejs-speech', false); + await preparePackage('speech', true); }); it('should pass system tests', async function () { - const result = await runSystemTest('nodejs-speech', false); + const result = await runSystemTest('speech', true); if (result === TestResult.SKIP) { this.skip(); } else if (result === TestResult.FAIL) { @@ -205,7 +221,8 @@ describe('Run system tests for some libraries', () => { // KMS api has IAM service injected from gax. All its IAM related test are in samples-test. // KMS is in the google-cloud-node monorepo - describe('kms', () => { + // Temporarily skipped to avoid circular dependency issue. + /*describe('kms', () => { before(async () => { await preparePackage('kms', true); }); @@ -217,5 +234,5 @@ describe('Run system tests for some libraries', () => { throw new Error('Test failed'); } }); - }); + });*/ }); diff --git a/test/test-application/package.json b/test/test-application/package.json index c2ad41876..3fc47060c 100644 --- a/test/test-application/package.json +++ b/test/test-application/package.json @@ -20,8 +20,8 @@ "start": "node build/src/index.js" }, "devDependencies": { - "mocha": "^9.1.4", - "typescript": "^4.5.5" + "mocha": "^10.0.1", + "typescript": "^5.1.6" }, "dependencies": { "@grpc/grpc-js": "~1.6.0", diff --git a/test/test-application/src/index.ts b/test/test-application/src/index.ts index 3f77d5ea3..a11abd838 100644 --- a/test/test-application/src/index.ts +++ b/test/test-application/src/index.ts @@ -15,13 +15,21 @@ */ 'use strict'; -import {EchoClient} from 'showcase-echo-client'; +import {EchoClient, SequenceServiceClient, protos} from 'showcase-echo-client'; import {ShowcaseServer} from 'showcase-server'; import * as assert from 'assert'; import {promises as fsp} from 'fs'; import * as path from 'path'; -import {protobuf, grpc, GoogleError, GoogleAuth} from 'google-gax'; +import { + protobuf, + grpc, + GoogleError, + GoogleAuth, + Status, + createBackoffSettings, + RetryOptions, +} from 'google-gax'; async function testShowcase() { const grpcClientOpts = { @@ -29,6 +37,12 @@ async function testShowcase() { sslCreds: grpc.credentials.createInsecure(), }; + const grpcClientOptsWithNewRetry = { + grpc, + sslCreds: grpc.credentials.createInsecure(), + newRetry: true, + }; + const fakeGoogleAuth = { getClient: async () => { return { @@ -41,14 +55,14 @@ async function testShowcase() { }, } as unknown as GoogleAuth; - const fallbackClientOpts = { + const restClientOpts = { fallback: true, protocol: 'http', - port: 1337, + port: 7469, auth: fakeGoogleAuth, }; - const restClientOpts = { + const restClientOptsCompat = { fallback: 'rest' as const, protocol: 'http', port: 7469, @@ -56,8 +70,14 @@ async function testShowcase() { }; const grpcClient = new EchoClient(grpcClientOpts); - const fallbackClient = new EchoClient(fallbackClientOpts); + + const grpcClientWithNewRetry = new EchoClient(grpcClientOptsWithNewRetry); + const grpcSequenceClientWithNewRetry = new SequenceServiceClient( + grpcClientOptsWithNewRetry + ); + const restClient = new EchoClient(restClientOpts); + const restClientCompat = new EchoClient(restClientOptsCompat); // assuming gRPC server is started locally await testEcho(grpcClient); @@ -69,85 +89,98 @@ async function testShowcase() { await testChat(grpcClient); await testWait(grpcClient); - await testEcho(fallbackClient); - await testEchoError(fallbackClient); - await testExpandThrows(fallbackClient); // fallback does not support server streaming - await testPagedExpand(fallbackClient); - await testPagedExpandAsync(fallbackClient); - await testCollectThrows(fallbackClient); // fallback does not support client streaming - await testChatThrows(fallbackClient); // fallback does not support bidi streaming - await testWait(fallbackClient); - - // await testEcho(fallbackClient); - // await testEchoError(fallbackClient); - // await testExpandThrows(fallbackClient); // fallback does not support server streaming - // await testPagedExpand(fallbackClient); - // await testPagedExpandAsync(fallbackClient); - // await testCollectThrows(fallbackClient); // fallback does not support client streaming - // await testChatThrows(fallbackClient); // fallback does not support bidi streaming - // await testWait(fallbackClient); - - // await testEcho(restClient); - // await testExpand(restClient); // REGAPIC supports server streaming - // await testPagedExpand(restClient); - // await testPagedExpandAsync(restClient); - // await testCollectThrows(restClient); // REGAPIC does not support client streaming - // await testChatThrows(restClient); // REGAPIC does not support bidi streaming - // await testWait(restClient); -} + await testEcho(restClient); + await testExpand(restClient); // REGAPIC supports server streaming + await testPagedExpand(restClient); + await testPagedExpandAsync(restClient); + await testCollectThrows(restClient); // REGAPIC does not support client streaming + await testChatThrows(restClient); // REGAPIC does not support bidi streaming + await testWait(restClient); + + await testEcho(restClientCompat); + await testExpand(restClientCompat); // REGAPIC supports server streaming + await testPagedExpand(restClientCompat); + await testPagedExpandAsync(restClientCompat); + await testCollectThrows(restClientCompat); // REGAPIC does not support client streaming + await testChatThrows(restClientCompat); // REGAPIC does not support bidi streaming + await testWait(restClientCompat); + // Testing with newRetry being true + await testServerStreamingRetryOptions(grpcSequenceClientWithNewRetry); + + await testServerStreamingRetriesWithShouldRetryFn( + grpcSequenceClientWithNewRetry + ); -function getStreamingSequenceRequest(){ - const request = new protos.google.showcase.v1beta1.CreateStreamingSequenceRequest() + await testServerStreamingRetrieswithRetryOptions( + grpcSequenceClientWithNewRetry + ); - let firstDelay = new protos.google.protobuf.Duration(); - firstDelay.nanos=150; + await testServerStreamingRetrieswithRetryRequestOptions( + grpcSequenceClientWithNewRetry + ); - let firstStatus = new protos.google.rpc.Status(); - firstStatus.code=14; - firstStatus.message="UNAVAILABLE"; + await testServerStreamingRetrieswithRetryRequestOptionsResumptionStrategy( + grpcSequenceClientWithNewRetry + ); - let firstResponse = new protos.google.showcase.v1beta1.StreamingSequence.Response(); - firstResponse.delay=firstDelay; - firstResponse.status=firstStatus; + await testServerStreamingRetrieswithRetryRequestOptionsErrorsOnBadResumptionStrategy( + grpcSequenceClientWithNewRetry + ); - // The Index you want the stream to fail or send the status - // This should be index + 1 so if you want to send status at index 0 - // you would provide firstResponse.sendStatusAtIndex=1 + await testServerStreamingThrowsClassifiedTransientError( + grpcSequenceClientWithNewRetry + ); - firstResponse.sendStatusAtIndex=1; - - let secondDelay = new protos.google.protobuf.Duration(); - secondDelay.nanos=150; + await testServerStreamingRetriesAndThrowsClassifiedTransientError( + grpcSequenceClientWithNewRetry + ); - let secondStatus = new protos.google.rpc.Status(); - secondStatus.code= 4; - secondStatus.message="DEADLINE_EXCEEDED"; + await testServerStreamingThrowsCannotSetTotalTimeoutMillisMaxRetries( + grpcSequenceClientWithNewRetry + ); + + await testEcho(grpcClientWithNewRetry); + await testEchoError(grpcClientWithNewRetry); + await testExpand(grpcClientWithNewRetry); + await testPagedExpand(grpcClientWithNewRetry); + await testPagedExpandAsync(grpcClientWithNewRetry); + await testCollect(grpcClientWithNewRetry); + await testChat(grpcClientWithNewRetry); + await testWait(grpcClientWithNewRetry); +} - let secondResponse = new protos.google.showcase.v1beta1.StreamingSequence.Response(); - secondResponse.delay=secondDelay; - secondResponse.status=secondStatus; - secondResponse.sendStatusAtIndex=2 +function createStreamingSequenceRequestFactory( + statusCodeList: Status[], + delayList: number[], + failIndexs: number[], + content: string +) { + const request = + new protos.google.showcase.v1beta1.CreateStreamingSequenceRequest(); + const streamingSequence = + new protos.google.showcase.v1beta1.StreamingSequence(); - let thirdDelay = new protos.google.protobuf.Duration(); - thirdDelay.nanos=500000; + for (let i = 0; i < statusCodeList.length; i++) { + const delay = new protos.google.protobuf.Duration(); + delay.seconds = delayList[i]; - let thirdStatus = new protos.google.rpc.Status(); - thirdStatus.code=0; - thirdStatus.message="OK"; + const status = new protos.google.rpc.Status(); + status.code = statusCodeList[i]; + status.message = statusCodeList[i].toString(); - let thirdResponse = new protos.google.showcase.v1beta1.StreamingSequence.Response(); - thirdResponse.delay=thirdDelay; - thirdResponse.status=thirdStatus; - thirdResponse.sendStatusAtIndex=11; + const response = + new protos.google.showcase.v1beta1.StreamingSequence.Response(); + response.delay = delay; + response.status = status; + response.responseIndex = failIndexs[i]; - let streamingSequence = new protos.google.showcase.v1beta1.StreamingSequence() - streamingSequence.responses = [firstResponse,secondResponse,thirdResponse]; - // streamingSequence.responses = []; + streamingSequence.responses.push(response); + streamingSequence.content = content; + } - streamingSequence.content = "This is testing the brand new and shiny StreamingSequence server 3"; - request.streamingsequence = streamingSequence + request.streamingSequence = streamingSequence; - return request + return request; } async function testEcho(client: EchoClient) { @@ -245,23 +278,6 @@ async function testExpand(client: EchoClient) { }); } -async function testExpandThrows(client: EchoClient) { - const words = ['nobody', 'ever', 'reads', 'test', 'input']; - const request = { - content: words.join(' '), - }; - assert.throws(() => { - const stream = client.expand(request); - const result: string[] = []; - stream.on('data', (response: {content: string}) => { - result.push(response.content); - }); - stream.on('end', () => { - assert.deepStrictEqual(words, result); - }); - }, /currently does not support/); -} - async function testPagedExpand(client: EchoClient) { const words = ['nobody', 'ever', 'reads', 'test', 'input']; const request = { @@ -308,7 +324,6 @@ async function testCollect(client: EchoClient) { resolve(result.content ?? ''); } }); -<<<<<<< Updated upstream for (const word of words) { const request = {content: word}; stream.write(request); @@ -323,22 +338,6 @@ async function testCollect(client: EchoClient) { }); const result = await promise; assert.strictEqual(result, words.join(' ')); -======= - return attemptStream - } - let attemptStream; - //TODO(coleleah): handle this more elegantly - if (sequence.responses){ - const numResponses = sequence.responses.length - console.log(numResponses); - - attemptStream = await multipleSequenceAttempts(numResponses) - }else{ - const numResponses = 3 - attemptStream = await multipleSequenceAttempts(numResponses) - - } ->>>>>>> Stashed changes } async function testCollectThrows(client: EchoClient) { @@ -465,6 +464,557 @@ async function testWait(client: EchoClient) { assert.deepStrictEqual(response.content, request.success.content); } +async function testServerStreamingRetryOptions(client: SequenceServiceClient) { + const finalData: string[] = []; + const backoffSettings = createBackoffSettings( + 100, + 1.2, + 1000, + null, + 1.5, + 3000, + 10000 + ); + + const retryOptions = new RetryOptions([], backoffSettings); + + const settings = { + retry: retryOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.OK], + [0.1], + [11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + + const response = await client.createStreamingSequence(request); + await new Promise((resolve, _) => { + const sequence = response[0]; + + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + attemptStream.on('data', (response: {content: string}) => { + finalData.push(response.content); + }); + attemptStream.on('error', () => { + //Do Nothing + }); + attemptStream.on('end', () => { + attemptStream.end(); + resolve(); + }); + }).then(() => { + assert.equal( + finalData.join(' '), + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + }); +} + +async function testServerStreamingRetrieswithRetryOptions( + client: SequenceServiceClient +) { + const finalData: string[] = []; + const backoffSettings = createBackoffSettings( + 100, + 1.2, + 1000, + null, + 1.5, + 3000, + 10000 + ); + + const retryOptions = new RetryOptions([14, 4], backoffSettings); + + const settings = { + retry: retryOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.UNAVAILABLE, Status.DEADLINE_EXCEEDED, Status.OK], + [0.1, 0.1, 0.1], + [1, 2, 11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + + const response = await client.createStreamingSequence(request); + await new Promise((resolve, _) => { + const sequence = response[0]; + + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + attemptStream.on('data', (response: {content: string}) => { + finalData.push(response.content); + }); + attemptStream.on('error', () => { + //Do Nothing + }); + attemptStream.on('end', () => { + attemptStream.end(); + resolve(); + }); + }).then(() => { + assert.equal( + finalData.join(' '), + 'This This is This is testing the brand new and shiny StreamingSequence server 3' + ); + }); +} + +async function testServerStreamingRetriesWithShouldRetryFn( + client: SequenceServiceClient +) { + const finalData: string[] = []; + const shouldRetryFn = function checkRetry(error: GoogleError) { + return [14, 4].includes(error.code!); + }; + + const backoffSettings = createBackoffSettings( + 100, + 1.2, + 1000, + null, + 1.5, + 3000, + 10000 + ); + + const retryOptions = new RetryOptions(shouldRetryFn, backoffSettings); + + const settings = { + retry: retryOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.UNAVAILABLE, Status.DEADLINE_EXCEEDED, Status.OK], + [0.1, 0.1, 0.1], + [1, 2, 11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + + const response = await client.createStreamingSequence(request); + await new Promise((resolve, _) => { + const sequence = response[0]; + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + attemptStream.on('data', (response: {content: string}) => { + finalData.push(response.content); + }); + attemptStream.on('error', () => { + // Do nothing + }); + attemptStream.on('end', () => { + attemptStream.end(); + resolve(); + }); + }).then(() => { + assert.equal( + finalData.join(' '), + 'This This is This is testing the brand new and shiny StreamingSequence server 3' + ); + }); +} + +async function testServerStreamingRetrieswithRetryRequestOptions( + client: SequenceServiceClient +) { + const finalData: string[] = []; + const retryRequestOptions = { + objectMode: true, + retries: 1, + maxRetryDelay: 70, + retryDelayMultiplier: 3, + totalTimeout: 650, + noResponseRetries: 3, + currentRetryAttempt: 0, + shouldRetryFn: function checkRetry(error: GoogleError) { + return [14, 4].includes(error.code!); + }, + }; + + const settings = { + retryRequestOptions: retryRequestOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.UNAVAILABLE, Status.DEADLINE_EXCEEDED, Status.OK], + [0.1, 0.1, 0.1], + [1, 2, 11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + + const response = await client.createStreamingSequence(request); + await new Promise((resolve, _) => { + const sequence = response[0]; + + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + attemptStream.on('data', (response: {content: string}) => { + finalData.push(response.content); + }); + attemptStream.on('error', () => { + // Do Nothing + }); + attemptStream.on('end', () => { + attemptStream.end(); + resolve(); + }); + }).then(() => { + assert.equal( + finalData.join(' '), + 'This This is This is testing the brand new and shiny StreamingSequence server 3' + ); + }); +} +async function testServerStreamingRetrieswithRetryRequestOptionsResumptionStrategy( + client: SequenceServiceClient +) { + const finalData: string[] = []; + const shouldRetryFn = (error: GoogleError) => { + return [4, 14].includes(error.code!); + }; + const backoffSettings = createBackoffSettings( + 10000, + 2.5, + 1000, + null, + 1.5, + 3000, + 600000 + ); + const getResumptionRequestFn = ( + originalRequest: protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest + ) => { + const newRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + newRequest.name = originalRequest.name; + newRequest.lastFailIndex = 5; + return newRequest; + }; + + const retryOptions = new RetryOptions( + shouldRetryFn, + backoffSettings, + getResumptionRequestFn + ); + + const settings = { + retry: retryOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.UNAVAILABLE, Status.DEADLINE_EXCEEDED, Status.OK], + [0.1, 0.1, 0.1], + [1, 2, 11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + const response = await client.createStreamingSequence(request); + await new Promise((resolve, _) => { + const sequence = response[0]; + + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + attemptStream.on('data', (response: {content: string}) => { + finalData.push(response.content); + }); + attemptStream.on('error', () => { + // do nothing + }); + attemptStream.on('end', () => { + attemptStream.end(); + resolve(); + }); + }).then(() => { + assert.deepStrictEqual( + finalData.join(' '), + 'This new and new and shiny StreamingSequence server 3' + ); + }); +} + +async function testServerStreamingRetrieswithRetryRequestOptionsErrorsOnBadResumptionStrategy( + client: SequenceServiceClient +) { + const shouldRetryFn = (error: GoogleError) => { + return [4, 14].includes(error.code!); + }; + const backoffSettings = createBackoffSettings( + 10000, + 2.5, + 1000, + null, + 1.5, + 3000, + 600000 + ); + const getResumptionRequestFn = () => { + // return a bad resumption strategy + return {}; + }; + + const retryOptions = new RetryOptions( + shouldRetryFn, + backoffSettings, + getResumptionRequestFn + ); + + const settings = { + retry: retryOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.UNAVAILABLE, Status.DEADLINE_EXCEEDED, Status.OK], + [0.1, 0.1, 0.1], + [1, 2, 11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + const allowedCodes = [4, 14]; + const response = await client.createStreamingSequence(request); + await new Promise((_, reject) => { + const sequence = response[0]; + + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + + attemptStream.on('error', (e: any) => { + if (!allowedCodes.includes(e.code!)) { + reject(e); + } + }); + }).then( + () => { + assert(false); + }, + (err: GoogleError) => { + assert.strictEqual(err.code, 3); + assert.match(err.message, /not classified as transient/); + } + ); +} + +async function testServerStreamingThrowsClassifiedTransientError( + client: SequenceServiceClient +) { + const backoffSettings = createBackoffSettings( + 100, + 1.2, + 1000, + null, + 1.5, + 3000, + 10000 + ); + const allowedCodes = [4]; + const retryOptions = new RetryOptions(allowedCodes, backoffSettings); + + const settings = { + retry: retryOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.UNAVAILABLE, Status.DEADLINE_EXCEEDED, Status.OK], + [0.1, 0.1, 0.1], + [1, 2, 11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + + const response = await client.createStreamingSequence(request); + await new Promise((_, reject) => { + const sequence = response[0]; + + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + attemptStream.on('error', (e: GoogleError) => { + if (!allowedCodes.includes(e.code!)) { + reject(e); + } + }); + }).then( + () => { + assert(false); + }, + (err: GoogleError) => { + assert.strictEqual(err.code, 3); + assert.match(err.message, /not classified as transient/); + } + ); +} + +async function testServerStreamingRetriesAndThrowsClassifiedTransientError( + client: SequenceServiceClient +) { + const backoffSettings = createBackoffSettings( + 100, + 1.2, + 1000, + null, + 1.5, + 3000, + 10000 + ); + const allowedCodes = [14]; + const retryOptions = new RetryOptions(allowedCodes, backoffSettings); + + const settings = { + retry: retryOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.UNAVAILABLE, Status.DEADLINE_EXCEEDED, Status.OK], + [0.1, 0.1, 0.1], + [1, 2, 11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + + const response = await client.createStreamingSequence(request); + await new Promise((_, reject) => { + const sequence = response[0]; + + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + attemptStream.on('error', (e: GoogleError) => { + if (!allowedCodes.includes(e.code!)) { + reject(e); + } + }); + }).then( + () => { + assert(false); + }, + (err: GoogleError) => { + assert.strictEqual(err.code, 3); + assert.match(err.message, /not classified as transient/); + } + ); +} + +async function testServerStreamingThrowsCannotSetTotalTimeoutMillisMaxRetries( + client: SequenceServiceClient +) { + const backoffSettings = createBackoffSettings( + 100, + 1.2, + 1000, + null, + 1.5, + 3000, + 10000 + ); + const allowedCodes = [14]; + backoffSettings.maxRetries = 1; + const retryOptions = new RetryOptions(allowedCodes, backoffSettings); + + const settings = { + retry: retryOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.UNAVAILABLE, Status.OK], + [0.1, 0.1], + [1, 11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + + const response = await client.createStreamingSequence(request); + await new Promise((_, reject) => { + const sequence = response[0]; + + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + attemptStream.on('error', (e: GoogleError) => { + if (!allowedCodes.includes(e.code!)) { + reject(e); + } + }); + }).then( + () => { + assert(false); + }, + (err: GoogleError) => { + assert.strictEqual(err.code, 3); + assert.match( + err.message, + /Cannot set both totalTimeoutMillis and maxRetries/ + ); + } + ); +} + async function main() { const showcaseServer = new ShowcaseServer(); try { diff --git a/test/unit/apiCallable.ts b/test/unit/apiCallable.ts index 988380ff4..628c6cf36 100644 --- a/test/unit/apiCallable.ts +++ b/test/unit/apiCallable.ts @@ -19,6 +19,7 @@ import {status} from '@grpc/grpc-js'; import {afterEach, describe, it} from 'mocha'; import * as sinon from 'sinon'; +import {RequestType} from '../../src/apitypes'; import * as gax from '../../src/gax'; import {GoogleError} from '../../src/googleError'; import * as utils from './utils'; @@ -66,8 +67,8 @@ describe('createApiCall', () => { const now = new Date(); const originalDeadline = now.getTime() + 100; const expectedDeadline = now.getTime() + 200; - assert(resp! > originalDeadline); - assert(resp! <= expectedDeadline); + assert((resp as any)! > originalDeadline); + assert((resp as any)! <= expectedDeadline); done(); }); }); @@ -139,20 +140,23 @@ describe('createApiCall', () => { done(); }); }); - - it('override just custom retry.retrycodes', done => { + it('override just custom retry.retryCodesOrShouldRetryFn with retry codes', done => { const initialRetryCodes = [1]; const overrideRetryCodes = [1, 2, 3]; // eslint-disable-next-line @typescript-eslint/no-explicit-any sinon.stub(retries, 'retryable').callsFake((func, retry): any => { - assert.strictEqual(retry.retryCodes, overrideRetryCodes); + try { + assert.strictEqual(retry.retryCodesOrShouldRetryFn, overrideRetryCodes); + return func; + } catch (err) { + done(err); + } return func; }); function func() { done(); } - const apiCall = createApiCall(func, { settings: { retry: gax.createRetryOptions(initialRetryCodes, { @@ -170,11 +174,50 @@ describe('createApiCall', () => { {}, { retry: { - retryCodes: overrideRetryCodes, + retryCodesOrShouldRetryFn: overrideRetryCodes, }, } ); }); + it('errors when you override just custom retry.retryCodesOrShouldRetryFn with a function on a non streaming call', async () => { + function neverRetry() { + return false; + } + const initialRetryCodes = [1]; + const overrideRetryCodes = neverRetry; + + function func() { + return Promise.resolve(); + } + + const apiCall = createApiCall(func, { + settings: { + retry: gax.createRetryOptions(initialRetryCodes, { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + totalTimeoutMillis: 4500, + }), + }, + }); + try { + await apiCall( + {}, + { + retry: { + retryCodesOrShouldRetryFn: overrideRetryCodes, + }, + } + ); + } catch (err: any) { + assert.strictEqual( + err.message, + 'Using a function to determine retry eligibility is only supported with server streaming calls' + ); + } + }); it('override just custom retry.backoffSettings', done => { const initialBackoffSettings = gax.createDefaultBackoffSettings(); @@ -212,6 +255,53 @@ describe('createApiCall', () => { } ); }); + + it('errors when a resumption strategy is passed for a non streaming call', async () => { + const initialBackoffSettings = gax.createDefaultBackoffSettings(); + const overriBackoffSettings = gax.createBackoffSettings( + 100, + 1.2, + 1000, + null, + 1.5, + 3000, + 4500 + ); + // "resumption" strategy is to just return the original request + const getResumptionRequestFn = (originalRequest: RequestType) => { + return originalRequest; + }; + + function func() { + Promise.resolve(); + } + const apiCall = createApiCall(func, { + settings: { + retry: gax.createRetryOptions( + [1], + initialBackoffSettings, + getResumptionRequestFn + ), + }, + }); + + try { + await apiCall( + {}, + { + retry: { + backoffSettings: overriBackoffSettings, + }, + } + ); + } catch (err) { + assert(err instanceof Error); + assert.strictEqual( + err.message, + 'Resumption strategy can only be used with server streaming retries' + ); + } + }); }); describe('Promise', () => { @@ -500,7 +590,8 @@ describe('retryable', () => { }); }); - // maxRetries is unsupported, and intended for internal use only. + // maxRetries is unsupported, and intended for internal use only or + // use with retry-request backwards compatibility it('errors when totalTimeoutMillis and maxRetries set', done => { const maxRetries = 5; const backoff = gax.createMaxRetriesBackoffSettings( diff --git a/test/unit/gax.ts b/test/unit/gax.ts index d7eb4a99d..62ff7cfea 100644 --- a/test/unit/gax.ts +++ b/test/unit/gax.ts @@ -74,11 +74,14 @@ const RETRY_DICT = { function expectRetryOptions(obj: gax.RetryOptions) { assert.ok(obj instanceof Object); - ['retryCodes', 'backoffSettings'].forEach(k => + ['retryCodesOrShouldRetryFn', 'backoffSettings'].forEach(k => // eslint-disable-next-line no-prototype-builtins assert.ok(obj.hasOwnProperty(k)) ); - assert.ok(obj.retryCodes instanceof Array); + assert.ok( + obj.retryCodesOrShouldRetryFn instanceof Array || + obj.retryCodesOrShouldRetryFn instanceof Function + ); expectBackoffSettings(obj.backoffSettings); } @@ -112,13 +115,13 @@ describe('gax construct settings', () => { assert.strictEqual(settings.timeout, 40000); assert.strictEqual(settings.apiName, SERVICE_NAME); expectRetryOptions(settings.retry); - assert.deepStrictEqual(settings.retry.retryCodes, [1, 2]); + assert.deepStrictEqual(settings.retry.retryCodesOrShouldRetryFn, [1, 2]); assert.strictEqual(settings.otherArgs, otherArgs); settings = defaults.pageStreamingMethod; assert.strictEqual(settings.timeout, 30000); expectRetryOptions(settings.retry); - assert.deepStrictEqual(settings.retry.retryCodes, [3]); + assert.deepStrictEqual(settings.retry.retryCodesOrShouldRetryFn, [3]); assert.strictEqual(settings.otherArgs, otherArgs); }); @@ -185,7 +188,9 @@ describe('gax construct settings', () => { let settings = defaults.bundlingMethod; let backoff = settings.retry.backoffSettings; assert.strictEqual(backoff.initialRetryDelayMillis, 1000); - assert.deepStrictEqual(settings.retry.retryCodes, [RETRY_DICT.code_a]); + assert.deepStrictEqual(settings.retry.retryCodesOrShouldRetryFn, [ + RETRY_DICT.code_a, + ]); assert.strictEqual(settings.timeout, 50000); /* page_streaming_method is unaffected because it's not specified in @@ -196,6 +201,8 @@ describe('gax construct settings', () => { assert.strictEqual(backoff.initialRetryDelayMillis, 100); assert.strictEqual(backoff.retryDelayMultiplier, 1.2); assert.strictEqual(backoff.maxRetryDelayMillis, 1000); - assert.deepStrictEqual(settings.retry.retryCodes, [RETRY_DICT.code_c]); + assert.deepStrictEqual(settings.retry.retryCodesOrShouldRetryFn, [ + RETRY_DICT.code_c, + ]); }); }); diff --git a/test/unit/grpc-fallback.ts b/test/unit/grpc-fallback.ts index e386c5eb6..53abfdb99 100644 --- a/test/unit/grpc-fallback.ts +++ b/test/unit/grpc-fallback.ts @@ -19,8 +19,6 @@ import * as assert from 'assert'; import {describe, it, beforeEach, afterEach, before, after} from 'mocha'; -import * as path from 'path'; -import * as fs from 'fs'; import * as nodeFetch from 'node-fetch'; import * as abortController from 'abort-controller'; import * as protobuf from 'protobufjs'; @@ -267,7 +265,7 @@ describe('grpc-fallback', () => { Promise.resolve({ ok: true, arrayBuffer: () => { - return Promise.resolve(responseType.encode(response).finish()); + return Promise.resolve(Buffer.from(JSON.stringify(response))); }, }) ); @@ -287,10 +285,23 @@ describe('grpc-fallback', () => { it('should handle an API error', done => { const requestObject = {content: 'test-content'}; // example of an actual google.rpc.Status error message returned by Language API - const fixtureName = path.resolve(__dirname, '..', 'fixtures', 'error.bin'); - const errorBin = fs.readFileSync(fixtureName); const expectedMessage = '3 INVALID_ARGUMENT: One of content, or gcs_content_uri must be set.'; + const jsonError = { + code: 400, // Bad request + message: expectedMessage, + details: [ + { + '@type': 'type.googleapis.com/google.rpc.BadRequest', + fieldViolations: [ + { + field: 'document.content', + description: 'Must have some text content to annotate.', + }, + ], + }, + ], + }; const expectedError = { code: 3, details: [ @@ -309,7 +320,7 @@ describe('grpc-fallback', () => { Promise.resolve({ ok: false, arrayBuffer: () => { - return Promise.resolve(errorBin); + return Promise.resolve(Buffer.from(JSON.stringify(jsonError))); }, }) ); diff --git a/test/unit/streaming.ts b/test/unit/streaming.ts index 5feab8dee..38efc7d34 100644 --- a/test/unit/streaming.ts +++ b/test/unit/streaming.ts @@ -21,12 +21,14 @@ import * as sinon from 'sinon'; import {afterEach, describe, it} from 'mocha'; import {PassThrough} from 'stream'; -import {GaxCallStream, GRPCCall} from '../../src/apitypes'; +import {GaxCallStream, GRPCCall, RequestType} from '../../src/apitypes'; import {createApiCall} from '../../src/createApiCall'; +import {StreamingApiCaller} from '../../src/streamingCalls/streamingApiCaller'; import * as gax from '../../src/gax'; import {StreamDescriptor} from '../../src/streamingCalls/streamDescriptor'; import * as streaming from '../../src/streamingCalls/streaming'; import {APICallback} from '../../src/apitypes'; +import * as warnings from '../../src/warnings'; import internal = require('stream'); import {StreamArrayParser} from '../../src/streamArrayParser'; import path = require('path'); @@ -35,6 +37,23 @@ import {GoogleError} from '../../src'; import {Metadata} from '@grpc/grpc-js'; function createApiCallStreaming( + func: + | Promise + | sinon.SinonSpy, internal.Transform | StreamArrayParser>, + type: streaming.StreamType, + rest?: boolean, + gaxStreamingRetries?: boolean +) { + const settings = new gax.CallSettings(); + return createApiCall( + //@ts-ignore + Promise.resolve(func), + settings, + new StreamDescriptor(type, rest, gaxStreamingRetries) + ) as GaxCallStream; +} + +function createApiCallStreamingWithNewLogic( func: | Promise | sinon.SinonSpy, internal.Transform | StreamArrayParser>, @@ -46,7 +65,7 @@ function createApiCallStreaming( //@ts-ignore Promise.resolve(func), settings, - new StreamDescriptor(type, rest) + new StreamDescriptor(type, rest, true) ) as GaxCallStream; } @@ -159,41 +178,6 @@ describe('streaming', () => { s.end(); }); - it('allows custome CallOptions.retry settings', done => { - sinon - .stub(streaming.StreamProxy.prototype, 'forwardEvents') - .callsFake(stream => { - assert(stream instanceof internal.Stream); - done(); - }); - const spy = sinon.spy((...args: Array<{}>) => { - assert.strictEqual(args.length, 3); - const s = new PassThrough({ - objectMode: true, - }); - return s; - }); - - const apiCall = createApiCallStreaming( - spy, - streaming.StreamType.SERVER_STREAMING - ); - - apiCall( - {}, - { - retry: gax.createRetryOptions([1], { - initialRetryDelayMillis: 100, - retryDelayMultiplier: 1.2, - maxRetryDelayMillis: 1000, - rpcTimeoutMultiplier: 1.5, - maxRpcTimeoutMillis: 3000, - totalTimeoutMillis: 4500, - }), - } - ); - }); - it('forwards metadata and status', done => { const responseMetadata = {metadata: true}; const status = {code: 0, metadata: responseMetadata}; @@ -264,8 +248,9 @@ describe('streaming', () => { s.end(s); }, 50); }); - it('cancels in the middle', done => { + const warnStub = sinon.stub(warnings, 'warn'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any function schedulePush(s: any, c: number) { const intervalId = setInterval(() => { @@ -297,7 +282,19 @@ describe('streaming', () => { func, streaming.StreamType.SERVER_STREAMING ); - const s = apiCall({}, undefined); + const s = apiCall( + {}, + { + retry: gax.createRetryOptions([5], { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + maxRetries: 0, + }), + } + ); let counter = 0; const expectedCount = 5; s.on('data', data => { @@ -313,6 +310,14 @@ describe('streaming', () => { assert.strictEqual(err, cancelError); done(); }); + assert.strictEqual(warnStub.callCount, 1); + assert( + warnStub.calledWith( + 'legacy_streaming_retry_behavior', + 'Legacy streaming retry behavior will not honor settings passed at call time or via client configuration. Please set gaxStreamingRetries to true to utilize passed retry settings. gaxStreamingRetries behavior will be set to true by default in future releases.', + 'DeprecationWarning' + ) + ); }); it('emit response when stream received metadata event', done => { @@ -409,7 +414,6 @@ describe('streaming', () => { s.push(null); s.on('end', () => { setTimeout(() => { - console.log('emit status event'); s.emit('status', expectedStatus); }, 10); }); @@ -462,6 +466,8 @@ describe('streaming', () => { }); it('emit parsed GoogleError', done => { + const warnStub = sinon.stub(warnings, 'warn'); + const errorInfoObj = { reason: 'SERVICE_DISABLED', domain: 'googleapis.com', @@ -488,6 +494,7 @@ describe('streaming', () => { details: 'Failed to read', metadata: metadata, }); + const spy = sinon.spy((...args: Array<{}>) => { assert.strictEqual(args.length, 3); const s = new PassThrough({ @@ -497,14 +504,33 @@ describe('streaming', () => { setImmediate(() => { s.emit('error', error); }); + setImmediate(() => { + s.emit('end'); + }); return s; }); const apiCall = createApiCallStreaming( spy, streaming.StreamType.SERVER_STREAMING ); - const s = apiCall({}, undefined); + + const s = apiCall( + {}, + { + retry: gax.createRetryOptions([5], { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + maxRetries: 0, + }), + } + ); + s.on('error', err => { + s.pause(); + s.destroy(); assert(err instanceof GoogleError); assert.deepStrictEqual(err.message, 'test error'); assert.strictEqual(err.domain, errorInfoObj.domain); @@ -515,6 +541,704 @@ describe('streaming', () => { ); done(); }); + s.on('end', () => { + done(); + }); + assert.strictEqual(warnStub.callCount, 1); + assert( + warnStub.calledWith( + 'legacy_streaming_retry_behavior', + 'Legacy streaming retry behavior will not honor settings passed at call time or via client configuration. Please set gaxStreamingRetries to true to utilize passed retry settings. gaxStreamingRetries behavior will be set to true by default in future releases.', + 'DeprecationWarning' + ) + ); + }); + + it('emit error and retry once', done => { + const firstError = Object.assign(new GoogleError('UNAVAILABLE'), { + code: 14, + details: 'UNAVAILABLE', + }); + let counter = 0; + const expectedStatus = {code: 0}; + const receivedData: string[] = []; + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + setImmediate(() => { + s.push('Hello'); + s.push('World'); + switch (counter) { + case 0: + s.emit('error', firstError); + counter++; + break; + case 1: + s.push('testing'); + s.push('retries'); + s.emit('status', expectedStatus); + counter++; + assert.deepStrictEqual( + receivedData.join(' '), + 'Hello World testing retries' + ); + done(); + break; + default: + break; + } + }); + return s; + }); + + const apiCall = createApiCallStreamingWithNewLogic( + spy, + streaming.StreamType.SERVER_STREAMING + ); + + const s = apiCall( + {}, + { + retry: gax.createRetryOptions([14], { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + maxRetries: 1, + }), + } + ); + let errorCount = 0; + s.on('data', data => { + receivedData.push(data); + }); + s.on('error', err => { + assert(err instanceof GoogleError); + switch (errorCount) { + case 0: + assert.deepStrictEqual(err.message, 'UNAVAILABLE'); + assert.strictEqual(err.code, 14); + break; + default: + break; + } + errorCount++; + }); + }); + + it('emit error and retry once with shouldRetryFn', done => { + const firstError = Object.assign(new GoogleError('UNAVAILABLE'), { + code: 14, + details: 'UNAVAILABLE', + }); + let counter = 0; + const expectedStatus = {code: 0}; + const receivedData: string[] = []; + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + setImmediate(() => { + s.push('Hello'); + s.push('World'); + switch (counter) { + case 0: + s.emit('error', firstError); + counter++; + break; + case 1: + s.push('testing'); + s.push('retries'); + s.emit('status', expectedStatus); + counter++; + assert.deepStrictEqual( + receivedData.join(' '), + 'Hello World testing retries' + ); + done(); + break; + default: + break; + } + }); + return s; + }); + + const apiCall = createApiCallStreamingWithNewLogic( + spy, + streaming.StreamType.SERVER_STREAMING + ); + + function retryCodesOrShouldRetryFn(error: GoogleError) { + return [14].includes(error.code!); + } + + const s = apiCall( + {}, + { + retry: gax.createRetryOptions(retryCodesOrShouldRetryFn, { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + maxRetries: 1, + }), + } + ); + let errorCount = 0; + s.on('data', data => { + receivedData.push(data); + }); + s.on('error', err => { + assert(err instanceof GoogleError); + switch (errorCount) { + case 0: + assert.deepStrictEqual(err.message, 'UNAVAILABLE'); + assert.strictEqual(err.code, 14); + break; + default: + break; + } + errorCount++; + }); + }); +}); + +describe('handles server streaming retries in gax when gaxStreamingRetries is enabled', () => { + afterEach(() => { + sinon.restore(); + }); + it('allows custom CallOptions.retry settings with shouldRetryFn instead of retryCodes and new retry behavior', done => { + sinon + .stub(streaming.StreamProxy.prototype, 'forwardEventsNewImplementation') + .callsFake((stream): any => { + assert(stream instanceof internal.Stream); + done(); + }); + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true //gaxStreamingRetries + ); + + apiCall( + {}, + { + retry: gax.createRetryOptions( + () => { + return true; + }, + { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + totalTimeoutMillis: 4500, + } + ), + } + ); + }); + it('allows custom CallOptions.retry settings with retryCodes and new retry behavior', done => { + sinon + .stub(streaming.StreamProxy.prototype, 'forwardEventsNewImplementation') + .callsFake((stream): any => { + assert(stream instanceof internal.Stream); + done(); + }); + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true //gaxStreamingRetries + ); + + apiCall( + {}, + { + retry: gax.createRetryOptions([1, 2, 3], { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + totalTimeoutMillis: 4500, + }), + } + ); + }); + it('allows the user to pass a custom resumption strategy', done => { + sinon + .stub(streaming.StreamProxy.prototype, 'forwardEventsNewImplementation') + .callsFake((stream, retry): any => { + console.log('hello i am here', retry); + assert(stream instanceof internal.Stream); + assert(retry.getResumptionRequestFn instanceof Function); + done(); + }); + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true //gaxStreamingRetries + ); + + // "resumption" strategy is to just return the original request + const getResumptionRequestFn = (originalRequest: RequestType) => { + return originalRequest; + }; + + apiCall( + {}, + { + retry: gax.createRetryOptions( + [1, 2, 3], + { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + totalTimeoutMillis: 4500, + }, + getResumptionRequestFn + ), + } + ); + }); + + it('throws an error when both retryRequestoptions and retryOptions are passed at call time when new retry behavior is enabled', done => { + //if this is reached, it means the settings merge in createAPICall did not fail properly + sinon + .stub(StreamingApiCaller.prototype, 'call') + .callsFake((apiCall, argument, settings, stream) => { + throw new Error("This shouldn't be happening"); + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true // ensure we're doing the new retries + ); + + const passedRetryRequestOptions = { + objectMode: false, + retries: 1, + maxRetryDelay: 70, + retryDelayMultiplier: 3, + totalTimeout: 650, + noResponseRetries: 3, + currentRetryAttempt: 0, + shouldRetryFn: function alwaysRetry() { + return true; + }, + }; + // make the call with both options passed at call time + try { + apiCall( + {}, + { + retryRequestOptions: passedRetryRequestOptions, + retry: gax.createRetryOptions([1], { + initialRetryDelayMillis: 300, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + totalTimeoutMillis: 4500, + }), + } + ); + } catch (err) { + assert(err instanceof Error); + assert.strictEqual( + err.toString(), + 'Error: Only one of retry or retryRequestOptions may be set' + ); + done(); + } + }); + it('throws a warning and converts retryRequestOptions for new retry behavior', done => { + const warnStub = sinon.stub(warnings, 'warn'); + sinon + .stub(StreamingApiCaller.prototype, 'call') + .callsFake((apiCall, argument, settings, stream) => { + try { + // Retry settings + // TODO: retries - one to one with maxRetries - this should be undefined if timeout is defined, I think + // //Backoff settings + assert(settings.retry); + assert(typeof settings.retryRequestOptions === 'undefined'); + assert.strictEqual( + settings.retry?.backoffSettings.maxRetryDelayMillis, + 70000 + ); + assert.strictEqual( + settings.retry?.backoffSettings.retryDelayMultiplier, + 3 + ); + // totalTimeout is undefined because maxRetries is passed + assert( + typeof settings.retry?.backoffSettings.totalTimeoutMillis === + 'undefined' + ); + + assert.strictEqual(settings.retry?.backoffSettings.maxRetries, 1); + assert( + typeof settings.retry.retryCodesOrShouldRetryFn === 'function' + ); + assert(settings.retry !== new gax.CallSettings().retry); + done(); + } catch (err) { + done(err); + } + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true // gaxStreamingRetries + ); + const passedRetryRequestOptions = { + objectMode: false, + retries: 1, + maxRetryDelay: 70, + retryDelayMultiplier: 3, + totalTimeout: 650, + noResponseRetries: 3, + currentRetryAttempt: 0, + shouldRetryFn: function alwaysRetry() { + return true; + }, + }; + // make the call with both options passed at call time + apiCall( + {}, + { + retryRequestOptions: passedRetryRequestOptions, + } + ); + + assert.strictEqual(warnStub.callCount, 1); + assert( + warnStub.calledWith( + 'retry_request_options', + 'retryRequestOptions will be deprecated in a future release. Please use retryOptions to pass retry options at call time', + 'DeprecationWarning' + ) + ); + }); + it('throws a warning and converts retryRequestOptions for new retry behavior - no maxRetries', done => { + const warnStub = sinon.stub(warnings, 'warn'); + sinon + .stub(StreamingApiCaller.prototype, 'call') + .callsFake((apiCall, argument, settings, stream) => { + try { + assert(settings.retry); + assert(typeof settings.retryRequestOptions === 'undefined'); + assert.strictEqual( + settings.retry?.backoffSettings.maxRetryDelayMillis, + 70000 + ); + assert.strictEqual( + settings.retry?.backoffSettings.retryDelayMultiplier, + 3 + ); + assert.strictEqual( + settings.retry?.backoffSettings.totalTimeoutMillis, + 650000 + ); + assert( + typeof settings.retry?.backoffSettings.maxRetries === 'undefined' + ); + assert( + typeof settings.retry.retryCodesOrShouldRetryFn === 'function' + ); + assert(settings.retry !== new gax.CallSettings().retry); + done(); + } catch (err) { + done(err); + } + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true // gaxStreamingRetries + ); + const passedRetryRequestOptions = { + objectMode: false, + maxRetryDelay: 70, + retryDelayMultiplier: 3, + totalTimeout: 650, + noResponseRetries: 3, + currentRetryAttempt: 0, + shouldRetryFn: function alwaysRetry() { + return true; + }, + }; + apiCall( + {}, + { + retryRequestOptions: passedRetryRequestOptions, + } + ); + + assert.strictEqual(warnStub.callCount, 1); + assert( + warnStub.calledWith( + 'retry_request_options', + 'retryRequestOptions will be deprecated in a future release. Please use retryOptions to pass retry options at call time', + 'DeprecationWarning' + ) + ); + }); +}); +describe('warns/errors about server streaming retry behavior when gaxStreamingRetries is disabled', () => { + afterEach(() => { + // restore 'call' stubs and 'warn' stubs + sinon.restore(); + }); + + // NO RETRY BEHAVIOR ENABLED + it('throws a warning when retryRequestOptions are passed', done => { + const warnStub = sinon.stub(warnings, 'warn'); + + // this exists to help resolve createApiCall + sinon + .stub(StreamingApiCaller.prototype, 'call') + .callsFake((apiCall, argument, settings, stream) => { + done(); + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + false // ensure we are NOT opted into the new retry behavior + ); + const passedRetryRequestOptions = { + objectMode: false, + retries: 1, + maxRetryDelay: 70, + retryDelayMultiplier: 3, + totalTimeout: 650, + noResponseRetries: 3, + currentRetryAttempt: 0, + shouldRetryFn: function alwaysRetry() { + return true; + }, + }; + // make the call with both options passed at call time + apiCall( + {}, + { + retryRequestOptions: passedRetryRequestOptions, + } + ); + assert.strictEqual(warnStub.callCount, 1); + assert( + warnStub.calledWith( + 'legacy_streaming_retry_request_behavior', + 'Legacy streaming retry behavior will not honor retryRequestOptions passed at call time. Please set gaxStreamingRetries to true to utilize passed retry settings. gaxStreamingRetries behavior will convert retryRequestOptions to retry parameters by default in future releases.', + 'DeprecationWarning' + ) + ); + }); + it('throws a warning when retry options are passed', done => { + const warnStub = sinon.stub(warnings, 'warn'); + // this exists to help resolve createApiCall + sinon + .stub(StreamingApiCaller.prototype, 'call') + .callsFake((apiCall, argument, settings, stream) => { + done(); + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + false // ensure we are NOT opted into the new retry behavior + ); + + // make the call with both options passed at call time + apiCall( + {}, + { + retry: gax.createRetryOptions([1], { + initialRetryDelayMillis: 300, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + totalTimeoutMillis: 4500, + }), + } + ); + assert.strictEqual(warnStub.callCount, 1); + assert( + warnStub.calledWith( + 'legacy_streaming_retry_behavior', + 'Legacy streaming retry behavior will not honor settings passed at call time or via client configuration. Please set gaxStreamingRetries to true to utilize passed retry settings. gaxStreamingRetries behavior will be set to true by default in future releases.', + 'DeprecationWarning' + ) + ); + }); + it('throws no warnings when when no retry options are passed', done => { + const warnStub = sinon.stub(warnings, 'warn'); + // this exists to help resolve createApiCall + sinon + .stub(StreamingApiCaller.prototype, 'call') + .callsFake((apiCall, argument, settings, stream) => { + done(); + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + false // ensure we are NOT opted into the new retry behavior + ); + + // make the call with neither retry option passed at call time + apiCall({}, {}); + assert.strictEqual(warnStub.callCount, 0); + }); + it('throws two warnings when when retry and retryRequestoptions are passed', done => { + const warnStub = sinon.stub(warnings, 'warn'); + // this exists to help resolve createApiCall + sinon + .stub(StreamingApiCaller.prototype, 'call') + .callsFake((apiCall, argument, settings, stream) => { + done(); + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + false // ensure we are NOT opted into the new retry behavior + ); + const passedRetryRequestOptions = { + objectMode: false, + retries: 1, + maxRetryDelay: 70, + retryDelayMultiplier: 3, + totalTimeout: 650, + noResponseRetries: 3, + currentRetryAttempt: 0, + shouldRetryFn: function alwaysRetry() { + return true; + }, + }; + // make the call with both retry options passed at call time + apiCall( + {}, + { + retryRequestOptions: passedRetryRequestOptions, + retry: gax.createRetryOptions([1], { + initialRetryDelayMillis: 300, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + totalTimeoutMillis: 4500, + }), + } + ); + assert.strictEqual(warnStub.callCount, 2); }); }); @@ -576,42 +1300,6 @@ describe('REST streaming apiCall return StreamArrayParser', () => { assert.deepStrictEqual(err.message, 'test error'); done(); }); - }); - - it('cancels StreamArrayParser in the middle', done => { - function schedulePush(s: StreamArrayParser, c: number) { - const intervalId = setInterval(() => { - s.push(c); - c++; - }, 10); - s.on('finish', () => { - clearInterval(intervalId); - }); - } - const spy = sinon.spy((...args: Array<{}>) => { - assert.strictEqual(args.length, 3); - const s = new StreamArrayParser(streamMethod); - schedulePush(s, 0); - return s; - }); - const apiCall = createApiCallStreaming( - //@ts-ignore - spy, - streaming.StreamType.SERVER_STREAMING, - true - ); - const s = apiCall({}, undefined); - let counter = 0; - const expectedCount = 5; - s.on('data', data => { - assert.strictEqual(data, counter); - counter++; - if (counter === expectedCount) { - s.cancel(); - } else if (counter > expectedCount) { - done(new Error('should not reach')); - } - }); s.on('end', () => { done(); }); diff --git a/test/unit/streamingRetryRequest.ts b/test/unit/streamingRetryRequest.ts new file mode 100644 index 000000000..dbdb204b6 --- /dev/null +++ b/test/unit/streamingRetryRequest.ts @@ -0,0 +1,122 @@ +// Copyright 2023 Google LLC + +// 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 + +// https://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. +/* eslint-disable @typescript-eslint/ban-ts-comment */ + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import {describe, it} from 'mocha'; +import {PassThrough} from 'stream'; + +import {CancellableStream, GaxCallStream, GRPCCall} from '../../src/apitypes'; +import {createApiCall} from '../../src/createApiCall'; +import * as gax from '../../src/gax'; +import {StreamDescriptor} from '../../src/streamingCalls/streamDescriptor'; +import * as streaming from '../../src/streamingCalls/streaming'; +import internal = require('stream'); +import {StreamArrayParser} from '../../src/streamArrayParser'; +import {streamingRetryRequest} from '../../src/streamingRetryRequest'; + +function createApiCallStreaming( + func: + | Promise + | sinon.SinonSpy, internal.Transform | StreamArrayParser>, + type: streaming.StreamType, + rest?: boolean, + gaxStreamingRetries?: boolean +) { + const settings = new gax.CallSettings(); + return createApiCall( + //@ts-ignore + Promise.resolve(func), + settings, + new StreamDescriptor(type, rest, gaxStreamingRetries) + ) as GaxCallStream; +} + +describe('retry-request', () => { + describe('streams', () => { + it('works with defaults in a stream', done => { + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + s.push({resources: [1, 2]}); + s.push({resources: [3, 4, 5]}); + s.push(null); + setImmediate(() => { + s.emit('metadata'); + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true + ); + + const retryStream = streamingRetryRequest( + null, + { + objectMode: true, + request: () => { + const stream = apiCall( + {}, + { + retry: gax.createRetryOptions([5], { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + maxRetries: 0, + }), + } + ) as CancellableStream; + return stream; + }, + }, + null + ) + .on('end', done()) + .on('data', (data: any) => { + console.log(data); + }); + assert.strictEqual(retryStream._readableState.objectMode, true); + }); + + it('throws request error', done => { + try { + streamingRetryRequest(null, null, null); + } catch (err: any) { + assert.match(err.message, /A request library must be provided/); + done(); + } + }); + + it('opts is a function and throws request error', done => { + try { + const opts = () => { + return true; + }; + streamingRetryRequest(null, opts, null); + } catch (err: any) { + assert.match(err.message, /A request library must be provided/); + done(); + } + }); + }); +}); diff --git a/tools/package.json b/tools/package.json index 53b34d894..08b137295 100644 --- a/tools/package.json +++ b/tools/package.json @@ -1,10 +1,10 @@ { "name": "gapic-tools", - "version": "0.1.7", + "version": "0.1.8", "description": "compiles, updates, and minifies protos", "main": "build/src/compileProtos.js", "files": [ - "build/tools/src", + "build/src", "!build/src/**/*.map" ], "scripts": { @@ -28,24 +28,24 @@ "author": "Google API Authors", "license": "Apache-2.0", "dependencies": { - "@types/rimraf": "^3.0.2", - "google-proto-files": "^3.0.0", - "protobufjs-cli": "1.1.1", + "google-gax": "^4.0.2", + "google-proto-files": "^4.0.0", + "protobufjs-cli": "1.1.2", + "rimraf": "^5.0.1", "uglify-js": "^3.17.0", - "walkdir": "^0.4.0", - "rimraf": "^3.0.2" + "walkdir": "^0.4.0" }, "repository": "googleapis/gax-nodejs", "devDependencies": { "@types/mocha": "^9.0.0", "@types/ncp": "^2.0.1", "@types/uglify-js": "^3.17.0", - "c8": "^7.0.0", - "gts": "^3.1.1", + "c8": "^8.0.0", + "gts": "^5.0.0", "mocha": "^9.0.0", "ncp": "^2.0.0", - "protobufjs": "7.2.3", - "typescript": "^4.6.4" + "protobufjs": "7.2.5", + "typescript": "^5.1.6" }, "engines": { "node": ">=14" diff --git a/tools/src/compileProtos.ts b/tools/src/compileProtos.ts index d6245d41e..10d5e24d7 100644 --- a/tools/src/compileProtos.ts +++ b/tools/src/compileProtos.ts @@ -22,6 +22,12 @@ import * as util from 'util'; import * as pbjs from 'protobufjs-cli/pbjs'; import * as pbts from 'protobufjs-cli/pbts'; +export const gaxProtos = path.join( + require.resolve('google-gax'), + '..', + '..', + 'protos' +); const readdir = util.promisify(fs.readdir); const readFile = util.promisify(fs.readFile); const writeFile = util.promisify(fs.writeFile); @@ -246,7 +252,7 @@ async function compileProtos( '-p', 'protos', '-p', - path.join(__dirname, '..', '..', '..', 'google-gax', 'build', 'protos'), + gaxProtos, '-o', jsonOutput, ]; @@ -264,7 +270,7 @@ async function compileProtos( '-p', 'protos', '-p', - path.join(__dirname, '..', '..', '..', 'google-gax', 'build', 'protos'), + gaxProtos, '-o', jsOutput, ]; diff --git a/tools/test/compileProtos.ts b/tools/test/compileProtos.ts index 87020b606..d9d68b49a 100644 --- a/tools/test/compileProtos.ts +++ b/tools/test/compileProtos.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; import * as fs from 'fs'; -import * as rimraf from 'rimraf'; +import {rimraf} from 'rimraf'; import * as util from 'util'; import * as path from 'path'; import * as protobuf from 'protobufjs'; @@ -25,7 +25,6 @@ import * as compileProtos from '../src/compileProtos'; const readFile = util.promisify(fs.readFile); const mkdir = util.promisify(fs.mkdir); -const rmrf = util.promisify(rimraf); const testDir = path.join(process.cwd(), '.compileProtos-test'); const resultDir = path.join(testDir, 'protos'); @@ -38,7 +37,7 @@ const expectedTSResultFile = path.join(resultDir, 'protos.d.ts'); describe('compileProtos tool', () => { beforeEach(async () => { if (fs.existsSync(testDir)) { - await rmrf(testDir); + await rimraf(testDir); } await mkdir(testDir); await mkdir(resultDir); @@ -50,6 +49,26 @@ describe('compileProtos tool', () => { process.chdir(cwd); }); + it('fetches gax from the appropriate place', async () => { + assert.deepStrictEqual(fs.readdirSync(compileProtos.gaxProtos), [ + 'compute_operations.d.ts', + 'compute_operations.js', + 'compute_operations.json', + 'google', + 'http.d.ts', + 'http.js', + 'iam_service.d.ts', + 'iam_service.js', + 'iam_service.json', + 'locations.d.ts', + 'locations.js', + 'locations.json', + 'operations.d.ts', + 'operations.js', + 'operations.json', + 'status.json', + ]); + }); it('compiles protos to JSON, JS, TS', async function () { this.timeout(20000); await compileProtos.main([ diff --git a/tools/test/fixtures/echo.js b/tools/test/fixtures/echo.js index 76c665675..e2db51487 100644 --- a/tools/test/fixtures/echo.js +++ b/tools/test/fixtures/echo.js @@ -16,13 +16,12 @@ // Lots of comments we can delete in uglify async function main() { - // Showcases auto-pagination functionality. - - // Let's say we have an API call that returns results grouped into pages. - // It accepts 4 parameters (just like gRPC stub calls do): - return 'SUCCESS'; - } - - main().catch(console.error); - // More comments - + // Showcases auto-pagination functionality. + + // Let's say we have an API call that returns results grouped into pages. + // It accepts 4 parameters (just like gRPC stub calls do): + return 'SUCCESS'; +} + +main().catch(console.error); +// More comments diff --git a/tools/test/minify.ts b/tools/test/minify.ts index 14d86eebf..19f814b0f 100644 --- a/tools/test/minify.ts +++ b/tools/test/minify.ts @@ -16,19 +16,18 @@ import * as assert from 'assert'; import {describe, it, beforeEach} from 'mocha'; import * as fs from 'fs'; import {promises as fsp} from 'fs'; -import * as rimraf from 'rimraf'; +import {rimraf} from 'rimraf'; import * as path from 'path'; import * as minify from '../src/minify'; import {promisify} from 'util'; -const rmrf = promisify(rimraf); const testDir = path.join(process.cwd(), '.minify-test'); const fixturesDir = path.join(__dirname, '..', 'test', 'fixtures'); describe('minify tool', () => { beforeEach(async () => { if (fs.existsSync(testDir)) { - await rmrf(testDir); + await rimraf(testDir); } await fsp.mkdir(testDir); }); diff --git a/tsconfig.json b/tsconfig.json index 0e984ded2..0921b23c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,9 @@ }, "include": [ "src/*.ts", + "tools/src/listProtos.ts", + "tools/src/minify.ts", + "tools/src/prepublish.ts", "src/*/*.ts", "test/system-test/*.ts", "test/unit/*.ts",