From 3ba689b5a8dddfb694a41da3492c70e6af72201f Mon Sep 17 00:00:00 2001 From: Alexander Fenster Date: Fri, 12 Apr 2019 13:52:16 -0700 Subject: [PATCH] refactor: a huge refactor of call handling (#467) * refactor: a huge refactor --- src/apiCallable.ts | 421 ------------ src/apiCaller.ts | 68 ++ src/apitypes.ts | 104 +++ src/bundling.ts | 599 ------------------ src/bundlingCalls/bundleApiCaller.ts | 84 +++ src/bundlingCalls/bundleDescriptor.ts | 95 +++ src/bundlingCalls/bundleExecutor.ts | 248 ++++++++ src/bundlingCalls/bundlingUtils.ts | 67 ++ src/bundlingCalls/task.ts | 266 ++++++++ src/call.ts | 133 ++++ src/createApiCall.ts | 106 ++++ src/descriptor.ts | 48 ++ src/gax.ts | 27 +- src/{GoogleError.ts => googleError.ts} | 2 +- src/grpc.ts | 2 +- src/index.ts | 14 +- src/isbrowser.ts | 37 +- src/longRunningCalls/longRunningApiCaller.ts | 108 ++++ src/longRunningCalls/longRunningDescriptor.ts | 65 ++ src/{ => longRunningCalls}/longrunning.ts | 134 +--- src/normalCalls/normalApiCaller.ts | 65 ++ src/normalCalls/retries.ts | 155 +++++ src/normalCalls/timeout.ts | 63 ++ src/operationsClient.ts | 63 +- src/pagedIteration.ts | 220 ------- src/paginationCalls/pageDescriptor.ts | 122 ++++ src/paginationCalls/pagedApiCaller.ts | 144 +++++ src/parserExtras.ts | 4 +- src/pathTemplate.ts | 4 +- src/pathTemplateParser.pegjs | 4 +- src/routingHeader.ts | 2 +- src/streamingCalls/streamDescriptor.ts | 57 ++ src/{ => streamingCalls}/streaming.ts | 90 +-- src/streamingCalls/streamingApiCaller.ts | 93 +++ src/warnings.ts | 37 +- system-test/system.ts | 37 +- test/apiCallable.ts | 49 +- test/bundling.ts | 70 +- test/gax.ts | 2 +- test/grpc.ts | 2 +- test/longrunning.ts | 79 +-- test/pagedIteration.ts | 24 +- test/path_template.ts | 4 +- test/routingHeader.ts | 3 +- test/streaming.ts | 49 +- test/utils.ts | 54 +- test/warning.ts | 37 +- tsconfig.json | 1 + 48 files changed, 2487 insertions(+), 1675 deletions(-) delete mode 100644 src/apiCallable.ts create mode 100644 src/apiCaller.ts create mode 100644 src/apitypes.ts delete mode 100644 src/bundling.ts create mode 100644 src/bundlingCalls/bundleApiCaller.ts create mode 100644 src/bundlingCalls/bundleDescriptor.ts create mode 100644 src/bundlingCalls/bundleExecutor.ts create mode 100644 src/bundlingCalls/bundlingUtils.ts create mode 100644 src/bundlingCalls/task.ts create mode 100644 src/call.ts create mode 100644 src/createApiCall.ts create mode 100644 src/descriptor.ts rename src/{GoogleError.ts => googleError.ts} (98%) create mode 100644 src/longRunningCalls/longRunningApiCaller.ts create mode 100644 src/longRunningCalls/longRunningDescriptor.ts rename src/{ => longRunningCalls}/longrunning.ts (73%) create mode 100644 src/normalCalls/normalApiCaller.ts create mode 100644 src/normalCalls/retries.ts create mode 100644 src/normalCalls/timeout.ts delete mode 100644 src/pagedIteration.ts create mode 100644 src/paginationCalls/pageDescriptor.ts create mode 100644 src/paginationCalls/pagedApiCaller.ts create mode 100644 src/streamingCalls/streamDescriptor.ts rename src/{ => streamingCalls}/streaming.ts (70%) create mode 100644 src/streamingCalls/streamingApiCaller.ts diff --git a/src/apiCallable.ts b/src/apiCallable.ts deleted file mode 100644 index 99e9ca3fe..000000000 --- a/src/apiCallable.ts +++ /dev/null @@ -1,421 +0,0 @@ -/* - * Copyright 2016, Google Inc. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following disclaimer - * in the documentation and/or other materials provided with the - * distribution. - * * Neither the name of Google Inc. nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -/** - * Provides function wrappers that implement page streaming and retrying. - */ - -import {status} from 'grpc'; -import {CallSettings, RetryOptions} from './gax'; -import {GoogleError} from './GoogleError'; - -export interface ArgumentFunction { - (argument: {}, callback: APICallback): void; -} - -/** - * @callback APICallback - * @param {?Error} error - * @param {?Object} response - */ -export type APICallback = - // tslint:disable-next-line no-any - (err: GoogleError|null, response?: any, next?: {}|null, - rawResponse?: {}|null) => void; - -/** - * @callback APIFunc - * @param {Object} argument - * @param {grpc.Metadata} metadata - * @param {Object} options - * @param {APICallback} callback - */ -export type APIFunc = - (argument: {}, metadata: {}, options: {}, callback: APICallback) => - Canceller; - -/** - * @callback APICall - * @param {Object} argument - * @param {CallOptions} callOptions - * @param {APICallback} callback - * @return {Promise|Stream|undefined} - */ -export interface APICall { - // tslint:disable-next-line no-any - (argument?: {}|null, callOptions?: {}|null, callback?: APICallback): any; -} - -export class Canceller { - callback?: APICallback; - cancelFunc?: () => void; - completed: boolean; - - /** - * Canceller manages callback, API calls, and cancellation - * of the API calls. - * @param {APICallback=} callback - * The callback to be called asynchronously when the API call - * finishes. - * @constructor - * @property {APICallback} callback - * The callback function to be called. - * @private - */ - constructor(callback?: APICallback) { - this.callback = callback; - this.completed = false; - } - - /** - * Cancels the ongoing promise. - */ - cancel(): void { - if (this.completed) { - return; - } - this.completed = true; - if (this.cancelFunc) { - this.cancelFunc(); - } else { - const error = new GoogleError('cancelled'); - error.code = status.CANCELLED; - this.callback!(error); - } - } - - - /** - * Call calls the specified function. Result will be used to fulfill - * the promise. - * - * @param {function(Object, APICallback=)} aFunc - * A function for an API call. - * @param {Object} argument - * A request object. - */ - call( - aFunc: (obj: {}, callback: APICallback) => PromiseCanceller, - argument: {}): void { - if (this.completed) { - return; - } - // tslint:disable-next-line no-any - const canceller = aFunc(argument, (...args: any[]) => { - this.completed = true; - setImmediate(this.callback!, ...args); - }); - this.cancelFunc = () => canceller.cancel(); - } -} - -// tslint:disable-next-line no-any -export interface CancellablePromise extends Promise { - cancel(): void; -} - -// tslint:disable-next-line no-any -export class PromiseCanceller extends Canceller { - promise: CancellablePromise; - /** - * PromiseCanceller is Canceller, but it holds a promise when - * the API call finishes. - * @param {Function} PromiseCtor - A constructor for a promise that implements - * the ES6 specification of promise. - * @constructor - * @private - */ - // tslint:disable-next-line variable-name - constructor(PromiseCtor: PromiseConstructor) { - super(); - this.promise = new PromiseCtor((resolve, reject) => { - this.callback = (err, response, next, rawResponse) => { - if (err) { - reject(err); - } else { - resolve([response, next, rawResponse]); - } - }; - }) as CancellablePromise; - this.promise.cancel = () => { - this.cancel(); - }; - } -} - -export interface ApiCallOtherArgs { - options?: {deadline?: Date;}; - headers?: {}; - metadataBuilder: (abTests?: {}, headers?: {}) => {}; -} - -/** - * Updates aFunc so that it gets called with the timeout as its final arg. - * - * This converts a function, aFunc, into another function with updated deadline. - * - * @private - * - * @param {APIFunc} aFunc - a function to be updated. - * @param {number} timeout - to be added to the original function as it final - * positional arg. - * @param {Object} otherArgs - the additional arguments to be passed to aFunc. - * @param {Object=} abTests - the A/B testing key/value pairs. - * @return {function(Object, APICallback)} - * the function with other arguments and the timeout. - */ -function addTimeoutArg( - aFunc: APIFunc, timeout: number, otherArgs: ApiCallOtherArgs, - abTests?: {}): ArgumentFunction { - // TODO: this assumes the other arguments consist of metadata and options, - // which is specific to gRPC calls. Remove the hidden dependency on gRPC. - return function timeoutFunc(argument, callback) { - 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 aFunc(argument, metadata!, options, callback); - }; -} - -/** - * Creates a function equivalent to aFunc, but that retries on certain - * exceptions. - * - * @private - * - * @param {APIFunc} aFunc - A function. - * @param {RetryOptions} retry - Configures the exceptions upon which the - * function eshould retry, and the parameters to the exponential backoff retry - * algorithm. - * @param {Object} otherArgs - the additional arguments to be passed to aFunc. - * @return {function(Object, APICallback)} A function that will retry. - */ -function retryable( - aFunc: APIFunc, retry: RetryOptions, - otherArgs: ApiCallOtherArgs): ArgumentFunction { - 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; - - /** - * Equivalent to ``aFunc``, but retries upon transient failure. - * - * Retrying is done through an exponential backoff algorithm configured - * by the options in ``retry``. - * @param {Object} argument The request object. - * @param {APICallback} callback The callback. - * @return {function()} cancel function. - */ - return function retryingFunc(argument: {}, callback: APICallback) { - let canceller: Canceller|null|void; - let timeoutId: NodeJS.Timer|null; - let now = new Date(); - let deadline: number; - if (retry.backoffSettings.totalTimeoutMillis) { - deadline = now.getTime() + retry.backoffSettings.totalTimeoutMillis; - } - let retries = 0; - const maxRetries = retry.backoffSettings.maxRetries!; - // TODO: define A/B testing values for retry behaviors. - - /** Repeat the API call as long as necessary. */ - function repeat() { - timeoutId = null; - if (deadline && now.getTime() >= deadline) { - const error = new GoogleError( - 'Retry total timeout exceeded before any response was received'); - error.code = status.DEADLINE_EXCEEDED; - callback(error); - return; - } - - if (retries && retries >= maxRetries) { - const error = new GoogleError( - 'Exceeded maximum number of retries before any ' + - 'response was received'); - error.code = status.DEADLINE_EXCEEDED; - callback(error); - return; - } - - retries++; - const toCall = addTimeoutArg(aFunc, timeout!, otherArgs); - canceller = toCall(argument, (err, response, next, rawResponse) => { - if (!err) { - callback(null, response, next, rawResponse); - return; - } - canceller = null; - if (retry.retryCodes.indexOf(err!.code!) < 0) { - err.note = 'Exception occurred in retry method that was ' + - 'not classified as transient'; - callback(err); - } else { - const toSleep = Math.random() * delay; - timeoutId = setTimeout(() => { - now = new Date(); - delay = Math.min(delay * delayMult, maxDelay); - timeout = Math.min( - timeout! * timeoutMult!, maxTimeout!, deadline - now.getTime()); - repeat(); - }, toSleep); - } - }); - } - - if (maxRetries && deadline!) { - const error = new GoogleError( - 'Cannot set both totalTimeoutMillis and maxRetries ' + - 'in backoffSettings.'); - error.code = status.INVALID_ARGUMENT; - callback(error); - } else { - repeat(); - } - - return { - cancel() { - if (timeoutId) { - clearTimeout(timeoutId); - } - if (canceller) { - canceller.cancel(); - } else { - const error = new GoogleError('cancelled'); - error.code = status.CANCELLED; - callback(error); - } - }, - }; - }; -} - -export interface NormalApiCallerSettings { - promise: PromiseConstructor; -} - -/** - * Creates an API caller for normal methods. - * - * @private - * @constructor - */ -export class NormalApiCaller { - init(settings: NormalApiCallerSettings, callback: APICallback): - PromiseCanceller|Canceller { - if (callback) { - return new Canceller(callback); - } - return new PromiseCanceller(settings.promise); - } - - wrap(func: Function): Function { - return func; - } - - call( - apiCall: APICall, argument: {}, settings: {}, - canceller: PromiseCanceller): void { - canceller.call(apiCall, argument); - } - - fail(canceller: PromiseCanceller, err: GoogleError): void { - canceller.callback!(err); - } - - result(canceller: PromiseCanceller) { - if (canceller.promise) { - return canceller.promise; - } - return; - } -} - -/** - * Converts an rpc call into an API call governed by the settings. - * - * In typical usage, `func` will be a promsie to a callable used to make an rpc - * request. This will mostly likely be a bound method from a request stub used - * to make an rpc call. It is not a direct function but a Promise instance, - * because of its asynchronism (typically, obtaining the auth information). - * - * The result is a function which manages the API call with the given settings - * and the options on the invocation. - * - * @param {Promise.} funcWithAuth - is a promise to be used to make - * a bare rpc call. This is a Promise instead of a bare function because - * the rpc call will be involeved with asynchronous authentications. - * @param {CallSettings} settings - provides the settings for this call - * @param {Object=} optDescriptor - optionally specify the descriptor for - * the method call. - * @return {APICall} func - a bound method on a request stub used - * to make an rpc call. - */ -export function createApiCall( - funcWithAuth: Promise, settings: CallSettings, - // tslint:disable-next-line no-any - optDescriptor?: any): APICall { - const apiCaller = - optDescriptor ? optDescriptor.apiCaller(settings) : new NormalApiCaller(); - - return function apiCallInner(request?, callOptions?, callback?) { - const thisSettings = settings.merge(callOptions); - - const status = apiCaller.init(thisSettings, callback); - funcWithAuth - .then(func => { - func = apiCaller.wrap(func); - const retry = thisSettings.retry; - if (retry && retry.retryCodes && retry.retryCodes.length > 0) { - return retryable( - func, thisSettings.retry!, - thisSettings.otherArgs as ApiCallOtherArgs); - } - return addTimeoutArg( - func, thisSettings.timeout, - thisSettings.otherArgs as ApiCallOtherArgs); - }) - .then(apiCall => { - apiCaller.call(apiCall, request, thisSettings, status); - }) - .catch(err => { - apiCaller.fail(status, err); - }); - return apiCaller.result(status); - }; -} diff --git a/src/apiCaller.ts b/src/apiCaller.ts new file mode 100644 index 000000000..7f0a6d3c2 --- /dev/null +++ b/src/apiCaller.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2019, Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {APICallback, CancellableStream, GRPCCall, ResultTuple, SimpleCallbackFunction} from './apitypes'; +import {CancellablePromise, OngoingCall, OngoingCallPromise} from './call'; +import {Descriptor} from './descriptor'; +import {CallSettings} from './gax'; +import {GoogleError} from './googleError'; +import {NormalApiCaller} from './normalCalls/normalApiCaller'; +import {StreamProxy} from './streamingCalls/streaming'; + +export interface ApiCallerSettings { + promise: PromiseConstructor; +} + +/** + * An interface for all kinds of API callers (normal, that just calls API, and + * all special ones: long-running, paginated, bundled, streaming). + */ +export interface APICaller { + init(settings: ApiCallerSettings, callback?: APICallback): OngoingCallPromise + |OngoingCall|StreamProxy; + wrap(func: GRPCCall): GRPCCall; + call( + apiCall: SimpleCallbackFunction, argument: {}, settings: {}, + canceller: OngoingCallPromise|OngoingCall|StreamProxy): void; + fail( + canceller: OngoingCallPromise|OngoingCall|CancellableStream, + err: GoogleError): void; + result(canceller: OngoingCallPromise|OngoingCall| + CancellableStream): CancellablePromise|CancellableStream; +} + +export function createAPICaller( + settings: CallSettings, descriptor: Descriptor|undefined): APICaller { + if (!descriptor) { + return new NormalApiCaller(); + } + return descriptor.getApiCaller(settings); +} diff --git a/src/apitypes.ts b/src/apitypes.ts new file mode 100644 index 000000000..f197eeb0d --- /dev/null +++ b/src/apitypes.ts @@ -0,0 +1,104 @@ +/* + * Copyright 2019 Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {Duplex} from 'stream'; + +import {CancellablePromise} from './call'; +import {CallOptions} from './gax'; +import {GoogleError} from './googleError'; +import {Operation} from './longRunningCalls/longrunning'; + +// gRPC functions return object with `.cancel()` method that can be used for +// canceling the ongoing call. +export interface GRPCCallResult { + cancel(): void; +} + +// All GAX calls take RequestType and return ResultTuple, which can contain up +// to three elements. The first element is always a response (post-processed for +// special types of call such as pagination or long-running), the second +// parameter is defined for paginated calls and stores the next page request +// object, the third parameter stores raw (unprocessed) response object in cases +// when it might be useful for users. +export type RequestType = {}; +export type ResponseType = {}|null; +export type NextPageRequestType = { + [index: string]: string +}|null; +export type RawResponseType = Operation|{}; +export type ResultTuple = [ + ResponseType, NextPageRequestType | undefined, RawResponseType | undefined +]; + +export interface SimpleCallbackFunction { + (argument: RequestType, callback: APICallback): GRPCCallResult; +} + +export type APICallback = + (err: GoogleError|null, response?: ResponseType, next?: NextPageRequestType, + rawResponse?: RawResponseType) => void; + +// The following five types mimic various gRPC calls (regular UnaryCall and +// various streaming calls). +export type UnaryCall = + (argument: {}, metadata: {}, options: {}, callback: APICallback) => + GRPCCallResult; +export type ServerStreamingCall = (argument: {}, metadata: {}, options: {}) => + Duplex&GRPCCallResult; +export type ClientStreamingCall = + (metadata: {}, options: {}, callback?: APICallback) => + Duplex&GRPCCallResult; +export type BiDiStreamingCall = (metadata: {}, options: {}) => + Duplex&GRPCCallResult; +export type GRPCCall = + UnaryCall|ServerStreamingCall|ClientStreamingCall|BiDiStreamingCall; + +// GAX wraps gRPC calls so that the wrapper functions return either a +// cancellable promise, or a stream (also cancellable!) +export type CancellableStream = Duplex&GRPCCallResult; +export type GaxCallResult = CancellablePromise|CancellableStream; +export interface GaxCallPromise { + (argument: {}, callOptions?: CallOptions, + callback?: APICallback): CancellablePromise; +} +export interface GaxCallStream { + (argument: {}, callOptions?: CallOptions, + callback?: APICallback): CancellableStream; +} +export interface GaxCall { + (argument: {}, callOptions?: CallOptions, + callback?: APICallback): GaxCallResult; +} +export interface GRPCCallOtherArgs { + options?: {deadline?: Date;}; + headers?: {}; + metadataBuilder: (abTests?: {}, headers?: {}) => {}; +} diff --git a/src/bundling.ts b/src/bundling.ts deleted file mode 100644 index 61f184ee3..000000000 --- a/src/bundling.ts +++ /dev/null @@ -1,599 +0,0 @@ -/* - * Copyright 2016, Google Inc. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following disclaimer - * in the documentation and/or other materials provided with the - * distribution. - * * Neither the name of Google Inc. nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -/** - * Provides behavior that supports request bundling. - */ - -import at = require('lodash.at'); -import {status} from 'grpc'; -import {NormalApiCaller, APICall, PromiseCanceller, APICallback} from './apiCallable'; -import {GoogleError} from './GoogleError'; -import {CallSettings} from './gax'; -import {warn} from './warnings'; - -/** - * A function which does nothing. Used for an empty cancellation funciton. - * @private - */ -function noop() {} - -/** - * Compute the identifier of the `obj`. The objects of the same ID - * will be bundled together. - * - * @param {Object} obj - The request object. - * @param {String[]} discriminatorFields - The array of field names. - * A field name may include '.' as a separator, which is used to - * indicate object traversal. - * @return {String|undefined} - the identifier string, or undefined if any - * discriminator. - * fields do not exist. - */ -export function computeBundleId(obj: {}, discriminatorFields: string[]) { - const ids: Array<{}|null> = []; - let hasIds = false; - for (let i = 0; i < discriminatorFields.length; ++i) { - const id = at(obj, discriminatorFields[i])[0]; - if (id === undefined) { - ids.push(null); - } else { - hasIds = true; - ids.push(id); - } - } - if (!hasIds) { - return undefined; - } - return JSON.stringify(ids); -} - -export interface SubResponseInfo { - field: string; - start?: number; - end?: number; -} - -export interface TaskElement {} - -export interface TaskData { - elements: TaskElement[]; - bytes: number; - callback: TaskCallback; - cancelled?: boolean; -} - -export interface TaskCallback extends APICallback { - id?: string; -} - -/** - * Creates a deep copy of the object with the consideration of subresponse - * fields for bundling. - * - * @param {Object} obj - The source object. - * @param {Object?} subresponseInfo - The information to copy the subset of - * the field for the response. Do nothing if it's null. - * @param {String} subresponseInfo.field - The field name. - * @param {number} subresponseInfo.start - The offset where the copying - * element should starts with. - * @param {number} subresponseInfo.end - The ending index where the copying - * region of the elements ends. - * @return {Object} The copied object. - * @private - */ -export function deepCopyForResponse( - // tslint:disable-next-line no-any - obj: any, subresponseInfo: SubResponseInfo|null) { - // tslint:disable-next-line no-any - let result: any; - if (obj === null) { - return null; - } - if (obj === undefined) { - return undefined; - } - if (Array.isArray(obj)) { - result = []; - obj.forEach(element => { - result.push(deepCopyForResponse(element, null)); - }); - return result; - } - // Some objects (such as ByteBuffer) have copy method. - if (obj.copy !== undefined) { - return obj.copy(); - } - // ArrayBuffer should be copied through slice(). - if (obj instanceof ArrayBuffer) { - return (obj as ArrayBuffer).slice(0); - } - if (typeof obj === 'object') { - result = {}; - Object.keys(obj).forEach(key => { - if (subresponseInfo && key === subresponseInfo.field && - Array.isArray(obj[key])) { - // Note that subresponses are not deep-copied. This is safe because - // those subresponses are not shared among callbacks. - result[key] = - obj[key].slice(subresponseInfo.start, subresponseInfo.end); - } else { - result[key] = deepCopyForResponse(obj[key], null); - } - }); - return result; - } - return obj; -} - -export class Task { - _apiCall: APICall; - _request: {[index: string]: TaskElement[]}; - _bundledField: string; - _subresponseField?: string|null; - _data: TaskData[]; - callCanceller?: PromiseCanceller; - - /** - * A task coordinates the execution of a single bundle. - * - * @param {function} apiCall - The function to conduct calling API. - * @param {Object} bundlingRequest - The base request object to be used - * for the actual API call. - * @param {string} bundledField - The name of the field in bundlingRequest - * to be bundled. - * @param {string=} subresponseField - The name of the field in the response - * to be passed to the callback. - * @constructor - * @private - */ - constructor( - apiCall: APICall, bundlingRequest: {}, bundledField: string, - subresponseField?: string|null) { - this._apiCall = apiCall; - this._request = bundlingRequest; - this._bundledField = bundledField; - this._subresponseField = subresponseField; - this._data = []; - } - - /** - * Returns the number of elements in a task. - * @return {number} The number of elements. - */ - getElementCount() { - let count = 0; - for (let i = 0; i < this._data.length; ++i) { - count += this._data[i].elements.length; - } - return count; - } - - /** - * Returns the total byte size of the elements in a task. - * @return {number} The byte size. - */ - getRequestByteSize() { - let size = 0; - for (let i = 0; i < this._data.length; ++i) { - size += this._data[i].bytes; - } - return size; - } - - /** - * Invokes the actual API call with current elements. - * @return {string[]} - the list of ids for invocations to be run. - */ - run() { - if (this._data.length === 0) { - return []; - } - const request = this._request; - const elements: TaskElement[] = []; - const ids: string[] = []; - for (let i = 0; i < this._data.length; ++i) { - elements.push.apply(elements, this._data[i].elements); - ids.push(this._data[i].callback.id!); - } - request[this._bundledField] = elements; - - const self = this; - this.callCanceller = - this._apiCall(request, (err: GoogleError|null, response?: {}) => { - const responses: Array<{}|null> = []; - if (err) { - self._data.forEach(() => { - responses.push(null); - }); - } else { - let subresponseInfo: SubResponseInfo|null = null; - if (self._subresponseField) { - subresponseInfo = { - field: self._subresponseField, - start: 0, - }; - } - self._data.forEach(data => { - if (subresponseInfo) { - subresponseInfo.end = - subresponseInfo.start! + data.elements.length; - } - responses.push(deepCopyForResponse(response, subresponseInfo)); - if (subresponseInfo) { - subresponseInfo.start = subresponseInfo.end; - } - }); - } - for (let i = 0; i < self._data.length; ++i) { - if (self._data[i].cancelled) { - const error = new GoogleError('cancelled'); - error.code = status.CANCELLED; - self._data[i].callback(error); - } else { - self._data[i].callback(err, responses[i]); - } - } - }); - return ids; - } - - /** - * Appends the list of elements into the task. - * @param {Object[]} elements - the new list of elements. - * @param {number} bytes - the byte size required to encode elements in the API. - * @param {APICallback} callback - the callback of the method call. - */ - extend(elements: TaskElement[], bytes: number, callback: TaskCallback) { - this._data.push({ - elements, - bytes, - callback, - }); - } - - /** - * Cancels a part of elements. - * @param {string} id - The identifier of the part of elements. - * @return {boolean} Whether the entire task will be canceled or not. - */ - cancel(id: string) { - if (this.callCanceller) { - let allCancelled = true; - this._data.forEach(d => { - if (d.callback.id === id) { - d.cancelled = true; - } - if (!d.cancelled) { - allCancelled = false; - } - }); - if (allCancelled) { - this.callCanceller.cancel(); - } - return allCancelled; - } - for (let i = 0; i < this._data.length; ++i) { - if (this._data[i].callback.id === id) { - const error = new GoogleError('cancelled'); - error.code = status.CANCELLED; - this._data[i].callback(error); - this._data.splice(i, 1); - break; - } - } - return this._data.length === 0; - } -} - -export interface BundleOptions { - elementCountLimit: number; - requestByteLimit: number; - elementCountThreshold: number; - requestByteThreshold: number; - delayThreshold: number; -} - -export class BundleExecutor { - _options: BundleOptions; - _descriptor: BundleDescriptor; - _tasks: {[index: string]: Task}; - _timers: {[index: string]: NodeJS.Timer}; - _invocations: {[index: string]: string}; - _invocationId: number; - /** - * Organizes requests for an api service that requires to bundle them. - * - * @param {BundleOptions} bundleOptions - configures strategy this instance - * uses when executing bundled functions. - * @param {BundleDescriptor} bundleDescriptor - the description of the bundling. - * @constructor - */ - constructor( - bundleOptions: BundleOptions, bundleDescriptor: BundleDescriptor) { - this._options = bundleOptions; - this._descriptor = bundleDescriptor; - this._tasks = {}; - this._timers = {}; - this._invocations = {}; - this._invocationId = 0; - } - - /** - * Schedule a method call. - * - * @param {function} apiCall - the function for an API call. - * @param {Object} request - the request object to be bundled with others. - * @param {APICallback} callback - the callback to be called when the method finished. - * @return {function()} - the function to cancel the scheduled invocation. - */ - schedule( - apiCall: APICall, request: {[index: string]: Array<{}>|string}, - callback?: TaskCallback) { - const bundleId = - computeBundleId(request, this._descriptor.requestDiscriminatorFields); - callback = (callback || noop) as TaskCallback; - if (bundleId === undefined) { - warn( - 'bundling_schedule_bundleid_undefined', - 'The request does not have enough information for request bundling. ' + - `Invoking immediately. Request: ${JSON.stringify(request)} ` + - `discriminator fields: ${ - this._descriptor.requestDiscriminatorFields}`); - return apiCall(request, callback); - } - if (request[this._descriptor.bundledField] === undefined) { - warn( - 'bundling_no_bundled_field', - `Request does not contain field ${ - this._descriptor.bundledField} that must present for bundling. ` + - `Invoking immediately. Request: ${JSON.stringify(request)}`); - return apiCall(request, callback); - } - - if (!(bundleId in this._tasks)) { - this._tasks[bundleId] = new Task( - apiCall, request, this._descriptor.bundledField, - this._descriptor.subresponseField); - } - let task = this._tasks[bundleId]; - callback.id = String(this._invocationId++); - this._invocations[callback.id] = bundleId; - - const bundledField = request[this._descriptor.bundledField] as Array<{}>; - const elementCount = bundledField.length; - let requestBytes = 0; - const self = this; - bundledField.forEach(obj => { - requestBytes += this._descriptor.byteLengthFunction(obj); - }); - - const countLimit = this._options.elementCountLimit || 0; - const byteLimit = this._options.requestByteLimit || 0; - - if ((countLimit > 0 && elementCount > countLimit) || - (byteLimit > 0 && requestBytes >= byteLimit)) { - let message; - if (countLimit > 0 && elementCount > countLimit) { - message = 'The number of elements ' + elementCount + - ' exceeds the limit ' + this._options.elementCountLimit; - } else { - message = 'The required bytes ' + requestBytes + ' exceeds the limit ' + - this._options.requestByteLimit; - } - const error = new GoogleError(message); - error.code = status.INVALID_ARGUMENT; - callback(error); - return { - cancel: noop, - }; - } - - const existingCount = task.getElementCount(); - const existingBytes = task.getRequestByteSize(); - - if ((countLimit > 0 && elementCount + existingCount >= countLimit) || - (byteLimit > 0 && requestBytes + existingBytes >= byteLimit)) { - this._runNow(bundleId); - this._tasks[bundleId] = new Task( - apiCall, request, this._descriptor.bundledField, - this._descriptor.subresponseField); - task = this._tasks[bundleId]; - } - - task.extend(bundledField, requestBytes, callback); - const ret = { - cancel() { - self._cancel(callback!.id!); - }, - }; - - const countThreshold = this._options.elementCountThreshold || 0; - const sizeThreshold = this._options.requestByteThreshold || 0; - if ((countThreshold > 0 && task.getElementCount() >= countThreshold) || - (sizeThreshold > 0 && task.getRequestByteSize() >= sizeThreshold)) { - this._runNow(bundleId); - return ret; - } - - if (!(bundleId in this._timers) && this._options.delayThreshold > 0) { - this._timers[bundleId] = setTimeout(() => { - delete this._timers[bundleId]; - this._runNow(bundleId); - }, this._options.delayThreshold); - } - - return ret; - } - - /** - * Clears scheduled timeout if it exists. - * - * @param {String} bundleId - the id for the task whose timeout needs to be - * cleared. - * @private - */ - _maybeClearTimeout(bundleId: string) { - if (bundleId in this._timers) { - const timerId = this._timers[bundleId]; - delete this._timers[bundleId]; - clearTimeout(timerId); - } - } - - /** - * Cancels an event. - * - * @param {String} id - The id for the event in the task. - * @private - */ - _cancel(id: string) { - if (!(id in this._invocations)) { - return; - } - const bundleId = this._invocations[id]; - if (!(bundleId in this._tasks)) { - return; - } - - const task = this._tasks[bundleId]; - delete this._invocations[id]; - if (task.cancel(id)) { - this._maybeClearTimeout(bundleId); - delete this._tasks[bundleId]; - } - } - - /** - * Invokes a task. - * - * @param {String} bundleId - The id for the task. - * @private - */ - _runNow(bundleId: string) { - if (!(bundleId in this._tasks)) { - warn('bundle_runnow_bundleid_unknown', `No such bundleid: ${bundleId}`); - return; - } - this._maybeClearTimeout(bundleId); - const task = this._tasks[bundleId]; - delete this._tasks[bundleId]; - - task.run().forEach(id => { - delete this._invocations[id]; - }); - } -} - -export class Bundleable extends NormalApiCaller { - bundler: BundleExecutor; - /** - * Creates an API caller that bundles requests. - * - * @private - * @constructor - * @param {BundleExecutor} bundler - bundles API calls. - */ - constructor(bundler: BundleExecutor) { - super(); - this.bundler = bundler; - } - - // tslint:disable-next-line no-any - call(apiCall: APICall, argument: {}, settings: CallSettings, status: any) { - if (settings.isBundling) { - status.call((argument: {}, callback: TaskCallback) => { - this.bundler.schedule(apiCall, argument, callback); - }, argument); - } else { - NormalApiCaller.prototype.call.call( - this, apiCall, argument, settings, status); - } - } -} - -export class BundleDescriptor { - bundledField: string; - requestDiscriminatorFields: string[]; - subresponseField: string|null; - byteLengthFunction: Function; - - /** - * Describes the structure of bundled call. - * - * requestDiscriminatorFields may include '.' as a separator, which is used to - * indicate object traversal. This allows fields in nested objects to be used - * to determine what request to bundle. - * - * @property {String} bundledField - * @property {String} requestDiscriminatorFields - * @property {String} subresponseField - * @property {Function} byteLengthFunction - * - * @param {String} bundledField - the repeated field in the request message - * that will have its elements aggregated by bundling. - * @param {String} requestDiscriminatorFields - a list of fields in the - * target request message class that are used to detemrine which request - * messages should be bundled together. - * @param {String} subresponseField - an optional field, when present it - * indicates the field in the response message that should be used to - * demultiplex the response into multiple response messages. - * @param {Function} byteLengthFunction - a function to obtain the byte - * length to be consumed for the bundled field messages. Because Node.JS - * protobuf.js/gRPC uses builtin Objects for the user-visible data and - * internally they are encoded/decoded in protobuf manner, this function - * is actually necessary to calculate the byte length. - * @constructor - */ - constructor( - bundledField: string, requestDiscriminatorFields: string[], - subresponseField: string|null, byteLengthFunction: Function) { - if (!byteLengthFunction && typeof subresponseField === 'function') { - byteLengthFunction = subresponseField; - subresponseField = null; - } - this.bundledField = bundledField; - this.requestDiscriminatorFields = requestDiscriminatorFields; - this.subresponseField = subresponseField; - this.byteLengthFunction = byteLengthFunction; - } - - - /** - * Returns a new API caller. - * @private - * @param {CallSettings} settings - the current settings. - * @return {Bundleable} - the new bundling API caller. - */ - apiCaller(settings: CallSettings) { - return new Bundleable(new BundleExecutor(settings.bundleOptions!, this)); - } -} diff --git a/src/bundlingCalls/bundleApiCaller.ts b/src/bundlingCalls/bundleApiCaller.ts new file mode 100644 index 000000000..001df3435 --- /dev/null +++ b/src/bundlingCalls/bundleApiCaller.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2019, Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {APICaller, ApiCallerSettings} from '../apiCaller'; +import {APICallback, GRPCCall, SimpleCallbackFunction} from '../apitypes'; +import {OngoingCall, OngoingCallPromise} from '../call'; +import {CallSettings} from '../gax'; +import {GoogleError} from '../googleError'; + +import {BundleExecutor} from './bundleExecutor'; +import {TaskCallback} from './task'; + +/** + * An implementation of APICaller for bundled calls. + * Uses BundleExecutor to do bundling. + */ +export class BundleApiCaller implements APICaller { + bundler: BundleExecutor; + + constructor(bundler: BundleExecutor) { + this.bundler = bundler; + } + + init(settings: ApiCallerSettings, callback?: APICallback): OngoingCallPromise + |OngoingCall { + if (callback) { + return new OngoingCall(callback); + } + return new OngoingCallPromise(settings.promise); + } + + wrap(func: GRPCCall): GRPCCall { + return func; + } + + call( + apiCall: SimpleCallbackFunction, argument: {}, settings: CallSettings, + status: OngoingCallPromise) { + if (!settings.isBundling) { + throw new GoogleError('Bundling enabled with no isBundling!'); + } + + status.call((argument: {}, callback: TaskCallback) => { + this.bundler.schedule(apiCall, argument, callback); + return status; + }, argument); + } + + fail(canceller: OngoingCallPromise, err: GoogleError): void { + canceller.callback!(err); + } + + result(canceller: OngoingCallPromise) { + return canceller.promise; + } +} diff --git a/src/bundlingCalls/bundleDescriptor.ts b/src/bundlingCalls/bundleDescriptor.ts new file mode 100644 index 000000000..6301cadee --- /dev/null +++ b/src/bundlingCalls/bundleDescriptor.ts @@ -0,0 +1,95 @@ +/* + * Copyright 2019, Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {Descriptor} from '../descriptor'; +import {CallSettings} from '../gax'; +import {NormalApiCaller} from '../normalCalls/normalApiCaller'; + +import {BundleApiCaller} from './bundleApiCaller'; +import {BundleExecutor} from './bundleExecutor'; + +/** + * A descriptor for calls that can be bundled into one call. + */ +export class BundleDescriptor implements Descriptor { + bundledField: string; + requestDiscriminatorFields: string[]; + subresponseField: string|null; + byteLengthFunction: Function; + + /** + * Describes the structure of bundled call. + * + * requestDiscriminatorFields may include '.' as a separator, which is used to + * indicate object traversal. This allows fields in nested objects to be used + * to determine what request to bundle. + * + * @property {String} bundledField + * @property {String} requestDiscriminatorFields + * @property {String} subresponseField + * @property {Function} byteLengthFunction + * + * @param {String} bundledField - the repeated field in the request message + * that will have its elements aggregated by bundling. + * @param {String} requestDiscriminatorFields - a list of fields in the + * target request message class that are used to detemrine which request + * messages should be bundled together. + * @param {String} subresponseField - an optional field, when present it + * indicates the field in the response message that should be used to + * demultiplex the response into multiple response messages. + * @param {Function} byteLengthFunction - a function to obtain the byte + * length to be consumed for the bundled field messages. Because Node.JS + * protobuf.js/gRPC uses builtin Objects for the user-visible data and + * internally they are encoded/decoded in protobuf manner, this function + * is actually necessary to calculate the byte length. + * @constructor + */ + constructor( + bundledField: string, requestDiscriminatorFields: string[], + subresponseField: string|null, byteLengthFunction: Function) { + if (!byteLengthFunction && typeof subresponseField === 'function') { + byteLengthFunction = subresponseField; + subresponseField = null; + } + this.bundledField = bundledField; + this.requestDiscriminatorFields = requestDiscriminatorFields; + this.subresponseField = subresponseField; + this.byteLengthFunction = byteLengthFunction; + } + + getApiCaller(settings: CallSettings) { + if (settings.isBundling === false) { + return new NormalApiCaller(); + } + return new BundleApiCaller( + new BundleExecutor(settings.bundleOptions!, this)); + } +} diff --git a/src/bundlingCalls/bundleExecutor.ts b/src/bundlingCalls/bundleExecutor.ts new file mode 100644 index 000000000..2dffbdc7d --- /dev/null +++ b/src/bundlingCalls/bundleExecutor.ts @@ -0,0 +1,248 @@ +/* + * Copyright 2019 Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {status} from 'grpc'; + +import {SimpleCallbackFunction} from '../apitypes'; +import {GoogleError} from '../googleError'; +import {warn} from '../warnings'; + +import {BundleDescriptor} from './bundleDescriptor'; +import {computeBundleId} from './bundlingUtils'; +import {Task, TaskCallback} from './task'; + +function noop() {} + +export interface BundleOptions { + elementCountLimit: number; + requestByteLimit: number; + elementCountThreshold: number; + requestByteThreshold: number; + delayThreshold: number; +} + +/** + * BundleExecutor stores several timers for each bundle (calls are bundled based + * on the options passed, each bundle has unique ID that is calculated based on + * field values). Each timer fires and sends a call after certain amount of + * time, and if a new request comes to the same bundle, the timer can be + * restarted. + */ +export class BundleExecutor { + _options: BundleOptions; + _descriptor: BundleDescriptor; + _tasks: {[index: string]: Task}; + _timers: {[index: string]: NodeJS.Timer}; + _invocations: {[index: string]: string}; + _invocationId: number; + /** + * Organizes requests for an api service that requires to bundle them. + * + * @param {BundleOptions} bundleOptions - configures strategy this instance + * uses when executing bundled functions. + * @param {BundleDescriptor} bundleDescriptor - the description of the bundling. + * @constructor + */ + constructor( + bundleOptions: BundleOptions, bundleDescriptor: BundleDescriptor) { + this._options = bundleOptions; + this._descriptor = bundleDescriptor; + this._tasks = {}; + this._timers = {}; + this._invocations = {}; + this._invocationId = 0; + } + + /** + * Schedule a method call. + * + * @param {function} apiCall - the function for an API call. + * @param {Object} request - the request object to be bundled with others. + * @param {APICallback} callback - the callback to be called when the method finished. + * @return {function()} - the function to cancel the scheduled invocation. + */ + schedule( + apiCall: SimpleCallbackFunction, + request: {[index: string]: Array<{}>|string}, callback?: TaskCallback) { + const bundleId = + computeBundleId(request, this._descriptor.requestDiscriminatorFields); + callback = (callback || noop) as TaskCallback; + if (bundleId === undefined) { + warn( + 'bundling_schedule_bundleid_undefined', + 'The request does not have enough information for request bundling. ' + + `Invoking immediately. Request: ${JSON.stringify(request)} ` + + `discriminator fields: ${ + this._descriptor.requestDiscriminatorFields}`); + return apiCall(request, callback); + } + if (request[this._descriptor.bundledField] === undefined) { + warn( + 'bundling_no_bundled_field', + `Request does not contain field ${ + this._descriptor.bundledField} that must present for bundling. ` + + `Invoking immediately. Request: ${JSON.stringify(request)}`); + return apiCall(request, callback); + } + + if (!(bundleId in this._tasks)) { + this._tasks[bundleId] = new Task( + apiCall, request, this._descriptor.bundledField, + this._descriptor.subresponseField); + } + let task = this._tasks[bundleId]; + callback.id = String(this._invocationId++); + this._invocations[callback.id] = bundleId; + + const bundledField = request[this._descriptor.bundledField] as Array<{}>; + const elementCount = bundledField.length; + let requestBytes = 0; + const self = this; + bundledField.forEach(obj => { + requestBytes += this._descriptor.byteLengthFunction(obj); + }); + + const countLimit = this._options.elementCountLimit || 0; + const byteLimit = this._options.requestByteLimit || 0; + + if ((countLimit > 0 && elementCount > countLimit) || + (byteLimit > 0 && requestBytes >= byteLimit)) { + let message; + if (countLimit > 0 && elementCount > countLimit) { + message = 'The number of elements ' + elementCount + + ' exceeds the limit ' + this._options.elementCountLimit; + } else { + message = 'The required bytes ' + requestBytes + ' exceeds the limit ' + + this._options.requestByteLimit; + } + const error = new GoogleError(message); + error.code = status.INVALID_ARGUMENT; + callback(error); + return { + cancel: noop, + }; + } + + const existingCount = task.getElementCount(); + const existingBytes = task.getRequestByteSize(); + + if ((countLimit > 0 && elementCount + existingCount >= countLimit) || + (byteLimit > 0 && requestBytes + existingBytes >= byteLimit)) { + this._runNow(bundleId); + this._tasks[bundleId] = new Task( + apiCall, request, this._descriptor.bundledField, + this._descriptor.subresponseField); + task = this._tasks[bundleId]; + } + + task.extend(bundledField, requestBytes, callback); + const ret = { + cancel() { + self._cancel(callback!.id!); + }, + }; + + const countThreshold = this._options.elementCountThreshold || 0; + const sizeThreshold = this._options.requestByteThreshold || 0; + if ((countThreshold > 0 && task.getElementCount() >= countThreshold) || + (sizeThreshold > 0 && task.getRequestByteSize() >= sizeThreshold)) { + this._runNow(bundleId); + return ret; + } + + if (!(bundleId in this._timers) && this._options.delayThreshold > 0) { + this._timers[bundleId] = setTimeout(() => { + delete this._timers[bundleId]; + this._runNow(bundleId); + }, this._options.delayThreshold); + } + + return ret; + } + + /** + * Clears scheduled timeout if it exists. + * + * @param {String} bundleId - the id for the task whose timeout needs to be + * cleared. + * @private + */ + private _maybeClearTimeout(bundleId: string) { + if (bundleId in this._timers) { + const timerId = this._timers[bundleId]; + delete this._timers[bundleId]; + clearTimeout(timerId); + } + } + + /** + * Cancels an event. + * + * @param {String} id - The id for the event in the task. + * @private + */ + private _cancel(id: string) { + if (!(id in this._invocations)) { + return; + } + const bundleId = this._invocations[id]; + if (!(bundleId in this._tasks)) { + return; + } + + const task = this._tasks[bundleId]; + delete this._invocations[id]; + if (task.cancel(id)) { + this._maybeClearTimeout(bundleId); + delete this._tasks[bundleId]; + } + } + + /** + * Invokes a task. + * + * @param {String} bundleId - The id for the task. + * @private + */ + _runNow(bundleId: string) { + if (!(bundleId in this._tasks)) { + warn('bundle_runnow_bundleid_unknown', `No such bundleid: ${bundleId}`); + return; + } + this._maybeClearTimeout(bundleId); + const task = this._tasks[bundleId]; + delete this._tasks[bundleId]; + + task.run().forEach(id => { + delete this._invocations[id]; + }); + } +} diff --git a/src/bundlingCalls/bundlingUtils.ts b/src/bundlingCalls/bundlingUtils.ts new file mode 100644 index 000000000..02e7fbab2 --- /dev/null +++ b/src/bundlingCalls/bundlingUtils.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2019 Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Provides behavior that supports request bundling. + */ + +import at = require('lodash.at'); +import {RequestType} from '../apitypes'; + +/** + * Compute the identifier of the `obj`. The objects of the same ID + * will be bundled together. + * + * @param {RequestType} obj - The request object. + * @param {String[]} discriminatorFields - The array of field names. + * A field name may include '.' as a separator, which is used to + * indicate object traversal. + * @return {String|undefined} - the identifier string, or undefined if any + * discriminator fields do not exist. + */ +export function computeBundleId( + obj: RequestType, discriminatorFields: string[]) { + const ids: Array<{}|null> = []; + let hasIds = false; + for (let i = 0; i < discriminatorFields.length; ++i) { + const id = at(obj, discriminatorFields[i])[0]; + if (id === undefined) { + ids.push(null); + } else { + hasIds = true; + ids.push(id); + } + } + if (!hasIds) { + return undefined; + } + return JSON.stringify(ids); +} diff --git a/src/bundlingCalls/task.ts b/src/bundlingCalls/task.ts new file mode 100644 index 000000000..17133bdd7 --- /dev/null +++ b/src/bundlingCalls/task.ts @@ -0,0 +1,266 @@ +/* + * Copyright 2019 Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {status} from 'grpc'; + +import {APICallback, GRPCCallResult, SimpleCallbackFunction} from '../apitypes'; +import {GoogleError} from '../googleError'; + +export interface SubResponseInfo { + field: string; + start?: number; + end?: number; +} + +export interface TaskElement {} + +export interface TaskData { + elements: TaskElement[]; + bytes: number; + callback: TaskCallback; + cancelled?: boolean; +} + +export interface TaskCallback extends APICallback { + id?: string; +} + +/** + * Creates a deep copy of the object with the consideration of subresponse + * fields for bundling. + * + * @param {Object} obj - The source object. + * @param {Object?} subresponseInfo - The information to copy the subset of + * the field for the response. Do nothing if it's null. + * @param {String} subresponseInfo.field - The field name. + * @param {number} subresponseInfo.start - The offset where the copying + * element should starts with. + * @param {number} subresponseInfo.end - The ending index where the copying + * region of the elements ends. + * @return {Object} The copied object. + * @private + */ +export function deepCopyForResponse( + // tslint:disable-next-line no-any + obj: any, subresponseInfo: SubResponseInfo|null) { + // tslint:disable-next-line no-any + let result: any; + if (obj === null) { + return null; + } + if (obj === undefined) { + return undefined; + } + if (Array.isArray(obj)) { + result = []; + obj.forEach(element => { + result.push(deepCopyForResponse(element, null)); + }); + return result; + } + // Some objects (such as ByteBuffer) have copy method. + if (obj.copy !== undefined) { + return obj.copy(); + } + // ArrayBuffer should be copied through slice(). + if (obj instanceof ArrayBuffer) { + return (obj as ArrayBuffer).slice(0); + } + if (typeof obj === 'object') { + result = {}; + Object.keys(obj).forEach(key => { + if (subresponseInfo && key === subresponseInfo.field && + Array.isArray(obj[key])) { + // Note that subresponses are not deep-copied. This is safe because + // those subresponses are not shared among callbacks. + result[key] = + obj[key].slice(subresponseInfo.start, subresponseInfo.end); + } else { + result[key] = deepCopyForResponse(obj[key], null); + } + }); + return result; + } + return obj; +} + +export class Task { + _apiCall: SimpleCallbackFunction; + _request: {[index: string]: TaskElement[];}; + _bundledField: string; + _subresponseField?: string|null; + _data: TaskData[]; + callCanceller?: GRPCCallResult; + /** + * A task coordinates the execution of a single bundle. + * + * @param {function} apiCall - The function to conduct calling API. + * @param {Object} bundlingRequest - The base request object to be used + * for the actual API call. + * @param {string} bundledField - The name of the field in bundlingRequest + * to be bundled. + * @param {string=} subresponseField - The name of the field in the response + * to be passed to the callback. + * @constructor + * @private + */ + constructor( + apiCall: SimpleCallbackFunction, bundlingRequest: {}, + bundledField: string, subresponseField?: string|null) { + this._apiCall = apiCall; + this._request = bundlingRequest; + this._bundledField = bundledField; + this._subresponseField = subresponseField; + this._data = []; + } + /** + * Returns the number of elements in a task. + * @return {number} The number of elements. + */ + getElementCount() { + let count = 0; + for (let i = 0; i < this._data.length; ++i) { + count += this._data[i].elements.length; + } + return count; + } + /** + * Returns the total byte size of the elements in a task. + * @return {number} The byte size. + */ + getRequestByteSize() { + let size = 0; + for (let i = 0; i < this._data.length; ++i) { + size += this._data[i].bytes; + } + return size; + } + /** + * Invokes the actual API call with current elements. + * @return {string[]} - the list of ids for invocations to be run. + */ + run() { + if (this._data.length === 0) { + return []; + } + const request = this._request; + const elements: TaskElement[] = []; + const ids: string[] = []; + for (let i = 0; i < this._data.length; ++i) { + elements.push.apply(elements, this._data[i].elements); + ids.push(this._data[i].callback.id!); + } + request[this._bundledField] = elements; + const self = this; + this.callCanceller = + this._apiCall(request, (err: GoogleError|null, response?: {}|null) => { + const responses: Array<{}|undefined> = []; + if (err) { + self._data.forEach(() => { + responses.push(undefined); + }); + } else { + let subresponseInfo: SubResponseInfo|null = null; + if (self._subresponseField) { + subresponseInfo = { + field: self._subresponseField, + start: 0, + }; + } + self._data.forEach(data => { + if (subresponseInfo) { + subresponseInfo.end = + subresponseInfo.start! + data.elements.length; + } + responses.push(deepCopyForResponse(response, subresponseInfo)); + if (subresponseInfo) { + subresponseInfo.start = subresponseInfo.end; + } + }); + } + for (let i = 0; i < self._data.length; ++i) { + if (self._data[i].cancelled) { + const error = new GoogleError('cancelled'); + error.code = status.CANCELLED; + self._data[i].callback(error); + } else { + self._data[i].callback(err, responses[i]); + } + } + }); + return ids; + } + /** + * Appends the list of elements into the task. + * @param {Object[]} elements - the new list of elements. + * @param {number} bytes - the byte size required to encode elements in the API. + * @param {APICallback} callback - the callback of the method call. + */ + extend(elements: TaskElement[], bytes: number, callback: TaskCallback) { + this._data.push({ + elements, + bytes, + callback, + }); + } + /** + * Cancels a part of elements. + * @param {string} id - The identifier of the part of elements. + * @return {boolean} Whether the entire task will be canceled or not. + */ + cancel(id: string) { + if (this.callCanceller) { + let allCancelled = true; + this._data.forEach(d => { + if (d.callback.id === id) { + d.cancelled = true; + } + if (!d.cancelled) { + allCancelled = false; + } + }); + if (allCancelled) { + this.callCanceller.cancel(); + } + return allCancelled; + } + for (let i = 0; i < this._data.length; ++i) { + if (this._data[i].callback.id === id) { + const error = new GoogleError('cancelled'); + error.code = status.CANCELLED; + this._data[i].callback(error); + this._data.splice(i, 1); + break; + } + } + return this._data.length === 0; + } +} diff --git a/src/call.ts b/src/call.ts new file mode 100644 index 000000000..7005c2ebd --- /dev/null +++ b/src/call.ts @@ -0,0 +1,133 @@ +/* + * Copyright 2019 Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {status} from 'grpc'; + +import {APICallback, NextPageRequestType, RawResponseType, RequestType, ResponseType, ResultTuple, SimpleCallbackFunction} from './apitypes'; +import {GoogleError} from './googleError'; + +export class OngoingCall { + callback?: APICallback; + cancelFunc?: () => void; + completed: boolean; + + /** + * OngoingCall manages callback, API calls, and cancellation + * of the API calls. + * @param {APICallback=} callback + * The callback to be called asynchronously when the API call + * finishes. + * @constructor + * @property {APICallback} callback + * The callback function to be called. + * @private + */ + constructor(callback?: APICallback) { + this.callback = callback; + this.completed = false; + } + + /** + * Cancels the ongoing promise. + */ + cancel(): void { + if (this.completed) { + return; + } + this.completed = true; + if (this.cancelFunc) { + this.cancelFunc(); + } else { + const error = new GoogleError('cancelled'); + error.code = status.CANCELLED; + this.callback!(error); + } + } + + /** + * Call calls the specified function. Result will be used to fulfill + * the promise. + * + * @param {SimpleCallbackFunction} func + * A function for an API call. + * @param {Object} argument + * A request object. + */ + call(func: SimpleCallbackFunction, argument: RequestType): void { + if (this.completed) { + return; + } + // tslint:disable-next-line no-any + const canceller = func(argument, (...args: any[]) => { + this.completed = true; + setImmediate(this.callback!, ...args); + }); + this.cancelFunc = () => canceller.cancel(); + } +} + +export interface CancellablePromise extends Promise { + cancel(): void; +} + +export class OngoingCallPromise extends OngoingCall { + promise: CancellablePromise; + /** + * GaxPromise is GRPCCallbackWrapper, but it holds a promise when + * the API call finishes. + * @param {Function} PromiseCtor - A constructor for a promise that implements + * the ES6 specification of promise. + * @constructor + * @private + */ + // tslint:disable-next-line variable-name + constructor(PromiseCtor: PromiseConstructor) { + super(); + this.promise = new PromiseCtor((resolve, reject) => { + this.callback = + (err: GoogleError|null, response?: ResponseType, + next?: NextPageRequestType|null, + rawResponse?: RawResponseType) => { + if (err) { + reject(err); + } else if (response !== undefined) { + resolve([response, next, rawResponse]); + } else { + throw new GoogleError( + 'Neither error nor response are defined'); + } + }; + }) as CancellablePromise; + this.promise.cancel = () => { + this.cancel(); + }; + } +} diff --git a/src/createApiCall.ts b/src/createApiCall.ts new file mode 100644 index 000000000..fa447e28e --- /dev/null +++ b/src/createApiCall.ts @@ -0,0 +1,106 @@ +/* + * Copyright 2019 Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Provides function wrappers that implement page streaming and retrying. + */ + +import {createAPICaller} from './apiCaller'; +import {APICallback, GaxCall, GRPCCall, GRPCCallOtherArgs, RequestType} from './apitypes'; +import {Descriptor} from './descriptor'; +import {CallOptions, CallSettings} from './gax'; +import {retryable} from './normalCalls/retries'; +import {addTimeoutArg} from './normalCalls/timeout'; + +/** + * Converts an rpc call into an API call governed by the settings. + * + * In typical usage, `func` will be a promise to a callable used to make an rpc + * request. This will mostly likely be a bound method from a request stub used + * to make an rpc call. It is not a direct function but a Promise instance, + * because of its asynchronism (typically, obtaining the auth information). + * + * The result is a function which manages the API call with the given settings + * and the options on the invocation. + * + * @param {Promise|GRPCCall} func - is either a promise to be used to make + * a bare RPC call, or just a bare RPC call. + * @param {CallSettings} settings - provides the settings for this call + * @param {Descriptor} descriptor - optionally specify the descriptor for + * the method call. + * @return {GaxCall} func - a bound method on a request stub used + * to make an rpc call. + */ +export function createApiCall( + func: Promise|GRPCCall, settings: CallSettings, + descriptor?: Descriptor): GaxCall { + // we want to be able to accept both promise resolving to a function and a + // 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); + + return (request: RequestType, callOptions?: CallOptions, + callback?: APICallback) => { + const thisSettings = settings.merge(callOptions); + + let currentApiCaller = apiCaller; + // special case: if bundling is disabled for this one call, + // use default API caller instead + if (settings.isBundling && !thisSettings.isBundling) { + currentApiCaller = createAPICaller(settings, undefined); + } + + const status = currentApiCaller.init(thisSettings, callback); + funcPromise + .then(func => { + func = currentApiCaller.wrap(func); + const retry = thisSettings.retry; + if (retry && retry.retryCodes && retry.retryCodes.length > 0) { + return retryable( + func, thisSettings.retry!, + thisSettings.otherArgs as GRPCCallOtherArgs); + } + return addTimeoutArg( + func, thisSettings.timeout, + thisSettings.otherArgs as GRPCCallOtherArgs); + }) + .then(apiCall => { + currentApiCaller.call(apiCall, request, thisSettings, status); + }) + .catch(err => { + currentApiCaller.fail(status, err); + }); + return currentApiCaller.result(status); + }; +} diff --git a/src/descriptor.ts b/src/descriptor.ts new file mode 100644 index 000000000..f0cb1ebf6 --- /dev/null +++ b/src/descriptor.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2019 Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +// Descriptors are passed by client libraries to `createApiCall` if a call must +// have special features, such as: support auto-pagination, be a long-running +// operation, a streaming method, or support bundling. Each type of descriptor +// can create its own apiCaller that will handle all the specifics for the given +// call type. + +import {APICaller} from './apiCaller'; +import {CallSettings} from './gax'; + +export interface Descriptor { + getApiCaller(settings: CallSettings): APICaller; +} + +export {LongRunningDescriptor as LongrunningDescriptor} from './longRunningCalls/longRunningDescriptor'; +export {PageDescriptor} from './paginationCalls/pageDescriptor'; +export {StreamDescriptor} from './streamingCalls/streamDescriptor'; +export {BundleDescriptor} from './bundlingCalls/bundleDescriptor'; diff --git a/src/gax.ts b/src/gax.ts index 066359840..eaa48d338 100644 --- a/src/gax.ts +++ b/src/gax.ts @@ -1,5 +1,5 @@ /** - * Copyright 2016, Google Inc. + * Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,7 +33,7 @@ * Google API Extensions */ -import {BundleOptions} from './bundling'; +import {BundleOptions} from './bundlingCalls/bundleExecutor'; /** * Encapsulates the overridable settings for a particular API call. @@ -162,14 +162,15 @@ export interface CallOptions { timeout?: number; retry?: RetryOptions|null; autoPaginate?: boolean; - pageToken?: number; + pageToken?: string; + pageSize?: number; maxResults?: number; maxRetries?: number; // tslint:disable-next-line no-any otherArgs?: {[index: string]: any}; bundleOptions?: BundleOptions|null; isBundling?: boolean; - longrunning?: boolean|null; + longrunning?: BackoffSettings; promise?: PromiseConstructor; } @@ -177,13 +178,14 @@ export class CallSettings { timeout: number; retry?: RetryOptions|null; autoPaginate?: boolean; - pageToken?: number; + pageToken?: string; + pageSize?: number; maxResults?: number; // tslint:disable-next-line no-any otherArgs: {[index: string]: any}; bundleOptions?: BundleOptions|null; isBundling: boolean; - longrunning?: boolean|null; + longrunning?: BackoffSettings; promise: PromiseConstructor; /** @@ -218,7 +220,8 @@ export class CallSettings { this.otherArgs = settings.otherArgs || {}; this.bundleOptions = settings.bundleOptions; this.isBundling = 'isBundling' in settings ? settings.isBundling! : true; - this.longrunning = 'longrunning' in settings ? settings.longrunning : null; + this.longrunning = + 'longrunning' in settings ? settings.longrunning : undefined; this.promise = 'promise' in settings ? settings.promise! : Promise; } @@ -238,6 +241,7 @@ export class CallSettings { let retry = this.retry; let autoPaginate = this.autoPaginate; let pageToken = this.pageToken; + let pageSize = this.pageSize; let maxResults = this.maxResults; let otherArgs = this.otherArgs; let isBundling = this.isBundling; @@ -259,6 +263,10 @@ export class CallSettings { pageToken = options.pageToken; } + if ('pageSize' in options) { + pageSize = options.pageSize; + } + if ('maxResults' in options) { maxResults = options.maxResults; } @@ -299,6 +307,7 @@ export class CallSettings { longrunning, autoPaginate, pageToken, + pageSize, maxResults, otherArgs, isBundling, @@ -366,6 +375,10 @@ export function createBackoffSettings( }; } +export function createDefaultBackoffSettings() { + return createBackoffSettings(100, 1.3, 60000, null, null, null, null); +} + /** * Parameters to the exponential backoff algorithm for retrying. * This function is unsupported, and intended for internal use only. diff --git a/src/GoogleError.ts b/src/googleError.ts similarity index 98% rename from src/GoogleError.ts rename to src/googleError.ts index ab43203ee..76b42e7aa 100644 --- a/src/GoogleError.ts +++ b/src/googleError.ts @@ -1,5 +1,5 @@ /* - * Copyright 2018, Google Inc. + * Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/grpc.ts b/src/grpc.ts index 319b36b7c..6b9d8c3a8 100644 --- a/src/grpc.ts +++ b/src/grpc.ts @@ -1,5 +1,5 @@ /* - * Copyright 2016, Google Inc. + * Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/index.ts b/src/index.ts index 110ba632d..5e8b0bd12 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ /** - * Copyright 2016, Google Inc. + * Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -34,15 +34,15 @@ import * as operationsClient from './operationsClient'; import * as routingHeader from './routingHeader'; export {GoogleAuth, GoogleAuthOptions} from 'google-auth-library'; -export {CancellablePromise, Canceller, createApiCall} from './apiCallable'; -export {BundleDescriptor, BundleExecutor} from './bundling'; +export {CancellablePromise, OngoingCall} from './call'; +export {createApiCall} from './createApiCall'; +export {BundleDescriptor, LongrunningDescriptor, PageDescriptor, StreamDescriptor} from './descriptor'; export {CallOptions, CallSettings, ClientConfig, constructSettings, RetryOptions} from './gax'; -export {GoogleError} from './GoogleError'; +export {GoogleError} from './googleError'; export {ClientStub, ClientStubOptions, GoogleProtoFilesRoot, GrpcClient, GrpcClientOptions, GrpcModule, GrpcObject, Metadata, MetadataValue} from './grpc'; -export {LongrunningDescriptor, Operation, operation} from './longrunning'; -export {PageDescriptor} from './pagedIteration'; +export {Operation, operation} from './longRunningCalls/longrunning'; export {PathTemplate} from './pathTemplate'; -export {StreamDescriptor, StreamType} from './streaming'; +export {StreamType} from './streamingCalls/streaming'; export {routingHeader}; function lro(options: GrpcClientOptions) { diff --git a/src/isbrowser.ts b/src/isbrowser.ts index 5ded266eb..ccfdc50db 100644 --- a/src/isbrowser.ts +++ b/src/isbrowser.ts @@ -1,17 +1,32 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. +/* + * Copyright 2019 Google LLC + * All rights reserved. * - * 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 + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: * - * http://www.apache.org/licenses/LICENSE-2.0 + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. * - * 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 SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ export function isBrowser(): boolean { diff --git a/src/longRunningCalls/longRunningApiCaller.ts b/src/longRunningCalls/longRunningApiCaller.ts new file mode 100644 index 000000000..b5618bfca --- /dev/null +++ b/src/longRunningCalls/longRunningApiCaller.ts @@ -0,0 +1,108 @@ +/* + * Copyright 2019, Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {APICaller, ApiCallerSettings} from '../apiCaller'; +import {APICallback, GRPCCall, SimpleCallbackFunction} from '../apitypes'; +import {OngoingCall, OngoingCallPromise} from '../call'; +import {BackoffSettings, CallOptions, CallSettings, createBackoffSettings, createDefaultBackoffSettings} from '../gax'; +import {GoogleError} from '../googleError'; + +import {Operation} from './longrunning'; +import {LongRunningDescriptor} from './longRunningDescriptor'; + +export class LongrunningApiCaller implements APICaller { + longrunningDescriptor: LongRunningDescriptor; + /** + * Creates an API caller that performs polling on a long running operation. + * + * @private + * @constructor + * @param {LongRunningDescriptor} longrunningDescriptor - Holds the + * decoders used for unpacking responses and the operationsClient + * used for polling the operation. + */ + constructor(longrunningDescriptor: LongRunningDescriptor) { + this.longrunningDescriptor = longrunningDescriptor; + } + + init(settings: ApiCallerSettings, callback?: APICallback): OngoingCallPromise + |OngoingCall { + if (callback) { + return new OngoingCall(callback); + } + return new OngoingCallPromise(settings.promise); + } + + + wrap(func: GRPCCall): GRPCCall { + return func; + } + + call( + apiCall: SimpleCallbackFunction, argument: {}, settings: CallOptions, + canceller: OngoingCallPromise) { + canceller.call((argument, callback) => { + return this._wrapOperation(apiCall, settings, argument, callback); + }, argument); + } + + private _wrapOperation( + apiCall: SimpleCallbackFunction, settings: CallOptions, argument: {}, + callback: APICallback) { + let backoffSettings: BackoffSettings|undefined = settings.longrunning; + if (!backoffSettings) { + backoffSettings = createDefaultBackoffSettings(); + } + + const longrunningDescriptor = this.longrunningDescriptor; + return apiCall( + argument, (err: GoogleError|null, rawResponse: {}|null|undefined) => { + if (err) { + callback(err, null, null, rawResponse as Operation); + return; + } + + const operation = new Operation( + rawResponse as Operation, longrunningDescriptor, backoffSettings!, + settings); + + callback(null, operation, rawResponse); + }); + } + + fail(canceller: OngoingCallPromise, err: GoogleError): void { + canceller.callback!(err); + } + + result(canceller: OngoingCallPromise) { + return canceller.promise; + } +} diff --git a/src/longRunningCalls/longRunningDescriptor.ts b/src/longRunningCalls/longRunningDescriptor.ts new file mode 100644 index 000000000..68af5bba7 --- /dev/null +++ b/src/longRunningCalls/longRunningDescriptor.ts @@ -0,0 +1,65 @@ +/* + * Copyright 2019 Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {Descriptor} from '../descriptor'; +import {CallSettings} from '../gax'; +import {Metadata} from '../grpc'; +import {OperationsClient} from '../operationsClient'; + +import {LongrunningApiCaller} from './longRunningApiCaller'; + +/** + * A callback to upack a google.protobuf.Any message. + */ +export interface AnyDecoder { + (message: {}): Metadata; +} + +/** + * A descriptor for long-running operations. + */ +export class LongRunningDescriptor implements Descriptor { + operationsClient: OperationsClient; + responseDecoder: AnyDecoder; + metadataDecoder: AnyDecoder; + + constructor( + operationsClient: OperationsClient, responseDecoder: AnyDecoder, + metadataDecoder: AnyDecoder) { + this.operationsClient = operationsClient; + this.responseDecoder = responseDecoder; + this.metadataDecoder = metadataDecoder; + } + + getApiCaller(settings: CallSettings) { + return new LongrunningApiCaller(this); + } +} diff --git a/src/longrunning.ts b/src/longRunningCalls/longrunning.ts similarity index 73% rename from src/longrunning.ts rename to src/longRunningCalls/longrunning.ts index db11ef721..07509e8e6 100644 --- a/src/longrunning.ts +++ b/src/longRunningCalls/longrunning.ts @@ -1,5 +1,5 @@ /* - * Copyright 2016, Google Inc. + * Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -31,21 +31,14 @@ import {EventEmitter} from 'events'; import {status} from 'grpc'; -import {APICall, APICallback, CancellablePromise, NormalApiCaller, PromiseCanceller} from './apiCallable'; -import {BackoffSettings, CallOptions, createBackoffSettings} from './gax'; -import {GoogleError} from './GoogleError'; -import {Metadata, MetadataValue} from './grpc'; -import {OperationsClient} from './operationsClient'; -/** - * A callback to upack a google.protobuf.Any message. - * @callback anyDecoder - * @param {google.protobuf.Any} message - The message to unpacked. - * @return {Object} - The unpacked message. - */ -export interface AnyDecoder { - (message: {}): Metadata; -} +import {GaxCallPromise, ResultTuple} from '../apitypes'; +import {CancellablePromise} from '../call'; +import {BackoffSettings, CallOptions} from '../gax'; +import {GoogleError} from '../googleError'; +import {Metadata, MetadataValue} from '../grpc'; + +import {LongRunningDescriptor} from './longRunningDescriptor'; /** * @callback GetOperationCallback @@ -58,101 +51,16 @@ export interface GetOperationCallback { (err?: Error|null, result?: {}, metadata?: {}, rawResponse?: Operation): void; } -export class LongrunningDescriptor { - operationsClient: OperationsClient; - responseDecoder: AnyDecoder; - metadataDecoder: AnyDecoder; - - /** - * Describes the structure of a page-streaming call. - * - * @property {OperationsClient} operationsClient - * @property {anyDecoder} responseDecoder - * @property {anyDecoder} metadataDecoder - * - * @param {OperationsClient} operationsClient - The client used to poll or - * cancel an operation. - * @param {anyDecoder=} responseDecoder - The decoder to unpack - * the response message. - * @param {anyDecoder=} metadataDecoder - The decoder to unpack - * the metadata message. - * - * @constructor - */ - constructor( - operationsClient: OperationsClient, responseDecoder: AnyDecoder, - metadataDecoder: AnyDecoder) { - this.operationsClient = operationsClient; - this.responseDecoder = responseDecoder; - this.metadataDecoder = metadataDecoder; - } - - apiCaller() { - return new LongrunningApiCaller(this); - } -} - -export class LongrunningApiCaller extends NormalApiCaller { - longrunningDescriptor: LongrunningDescriptor; - /** - * Creates an API caller that performs polling on a long running operation. - * - * @private - * @constructor - * @param {LongrunningDescriptor} longrunningDescriptor - Holds the - * decoders used for unpacking responses and the operationsClient - * used for polling the operation. - */ - constructor(longrunningDescriptor: LongrunningDescriptor) { - super(); - this.longrunningDescriptor = longrunningDescriptor; - } - - - call( - apiCall: APICall, argument: {}, settings: CallOptions, - canceller: PromiseCanceller) { - canceller.call((argument, callback) => { - return this._wrapOperation(apiCall, settings, argument, callback); - }, argument); - } - - _wrapOperation( - apiCall: APICall, settings: CallOptions, argument: {}, - callback: APICallback) { - // TODO: this code defies all logic, and just can't be accurate. - // tslint:disable-next-line no-any - let backoffSettings: any = settings.longrunning; - if (!backoffSettings) { - backoffSettings = - createBackoffSettings(100, 1.3, 60000, null, null, null, null); - } - - const longrunningDescriptor = this.longrunningDescriptor; - return apiCall(argument, (err: Error, rawResponse: Operation) => { - if (err) { - callback(err, null, rawResponse); - return; - } - - const operation = new Operation( - rawResponse, longrunningDescriptor, backoffSettings!, settings); - - callback(null, operation, rawResponse); - }); - } -} - export class Operation extends EventEmitter { completeListeners: number; hasActiveListeners: boolean; latestResponse: Operation; - longrunningDescriptor: LongrunningDescriptor; + longrunningDescriptor: LongRunningDescriptor; result: {}|null; metadata: Metadata|null; backoffSettings: BackoffSettings; _callOptions?: CallOptions; - currentCallPromise_?: CancellablePromise; + currentCallPromise_?: CancellablePromise; name?: string; done?: boolean; error?: GoogleError; @@ -164,15 +72,15 @@ export class Operation extends EventEmitter { * @constructor * * @param {google.longrunning.Operation} grpcOp - The operation to be wrapped. - * @param {LongrunningDescriptor} longrunningDescriptor - This defines the + * @param {LongRunningDescriptor} longrunningDescriptor - This defines the * operations service client and unpacking mechanisms for the operation. * @param {BackoffSettings} backoffSettings - The backoff settings used in * in polling the operation. - * @param {CallOptions=} callOptions - CallOptions used in making get operation + * @param {CallOptions} callOptions - CallOptions used in making get operation * requests. */ constructor( - grpcOp: Operation, longrunningDescriptor: LongrunningDescriptor, + grpcOp: Operation, longrunningDescriptor: LongRunningDescriptor, backoffSettings: BackoffSettings, callOptions?: CallOptions) { super(); this.completeListeners = 0; @@ -227,7 +135,8 @@ export class Operation extends EventEmitter { this.currentCallPromise_.cancel(); } const operationsClient = this.longrunningDescriptor.operationsClient; - return operationsClient.cancelOperation({name: this.latestResponse.name}); + return operationsClient.cancelOperation({name: this.latestResponse.name}) as + CancellablePromise; } /** @@ -274,12 +183,13 @@ export class Operation extends EventEmitter { return promisifyResponse(); } - this.currentCallPromise_ = operationsClient.getOperation( - {name: this.latestResponse.name}, this._callOptions!); + this.currentCallPromise_ = + (operationsClient.getOperation as GaxCallPromise)( + {name: this.latestResponse.name}, this._callOptions!); const noCallbackPromise = this.currentCallPromise_!.then(responses => { - self.latestResponse = responses[0]; - self._unpackResponse(responses[0], callback); + self.latestResponse = responses[0] as Operation; + self._unpackResponse(responses[0] as Operation, callback); return promisifyResponse()!; }); @@ -423,7 +333,7 @@ export class Operation extends EventEmitter { * @constructor * * @param {google.longrunning.Operation} op - The operation to be wrapped. - * @param {LongrunningDescriptor} longrunningDescriptor - This defines the + * @param {LongRunningDescriptor} longrunningDescriptor - This defines the * operations service client and unpacking mechanisms for the operation. * @param {BackoffSettings} backoffSettings - The backoff settings used in * in polling the operation. @@ -431,7 +341,7 @@ export class Operation extends EventEmitter { * requests. */ export function operation( - op: Operation, longrunningDescriptor: LongrunningDescriptor, + op: Operation, longrunningDescriptor: LongRunningDescriptor, backoffSettings: BackoffSettings, callOptions?: CallOptions) { return new Operation(op, longrunningDescriptor, backoffSettings, callOptions); } diff --git a/src/normalCalls/normalApiCaller.ts b/src/normalCalls/normalApiCaller.ts new file mode 100644 index 000000000..4a01b3e52 --- /dev/null +++ b/src/normalCalls/normalApiCaller.ts @@ -0,0 +1,65 @@ +/* + * Copyright 2019, Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +import {APICaller, ApiCallerSettings} from '../apiCaller'; +import {APICallback, GRPCCall, SimpleCallbackFunction} from '../apitypes'; +import {OngoingCall, OngoingCallPromise} from '../call'; +import {GoogleError} from '../googleError'; + +/** + * Creates an API caller for regular unary methods. + */ +export class NormalApiCaller implements APICaller { + init(settings: ApiCallerSettings, callback?: APICallback): OngoingCallPromise + |OngoingCall { + if (callback) { + return new OngoingCall(callback); + } + return new OngoingCallPromise(settings.promise); + } + + wrap(func: GRPCCall): GRPCCall { + return func; + } + + call( + apiCall: SimpleCallbackFunction, argument: {}, settings: {}, + canceller: OngoingCallPromise): void { + canceller.call(apiCall, argument); + } + + fail(canceller: OngoingCallPromise, err: GoogleError): void { + canceller.callback!(err); + } + + result(canceller: OngoingCallPromise) { + return canceller.promise; + } +} diff --git a/src/normalCalls/retries.ts b/src/normalCalls/retries.ts new file mode 100644 index 000000000..bccd72306 --- /dev/null +++ b/src/normalCalls/retries.ts @@ -0,0 +1,155 @@ +/* + * Copyright 2019 Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {status} from 'grpc'; + +import {APICallback, GRPCCall, GRPCCallOtherArgs, GRPCCallResult, RequestType, SimpleCallbackFunction} from '../apitypes'; +import {RetryOptions} from '../gax'; +import {GoogleError} from '../googleError'; + +import {addTimeoutArg} from './timeout'; + +/** + * Creates a function equivalent to func, but that retries on certain + * exceptions. + * + * @private + * + * @param {GRPCCall} func - A function. + * @param {RetryOptions} retry - Configures the exceptions upon which the + * function eshould retry, and the parameters to the exponential backoff retry + * algorithm. + * @param {GRPCCallOtherArgs} otherArgs - the additional arguments to be passed to func. + * @return {SimpleCallbackFunction} A function that will retry. + */ +export function retryable( + func: GRPCCall, retry: RetryOptions, + otherArgs: GRPCCallOtherArgs): SimpleCallbackFunction { + 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; + + /** + * Equivalent to ``func``, but retries upon transient failure. + * + * Retrying is done through an exponential backoff algorithm configured + * by the options in ``retry``. + * @param {RequestType} argument The request object. + * @param {APICallback} callback The callback. + * @return {GRPCCall} + */ + return (argument: RequestType, callback: APICallback) => { + let canceller: GRPCCallResult|null; + let timeoutId: NodeJS.Timer|null; + let now = new Date(); + let deadline: number; + if (retry.backoffSettings.totalTimeoutMillis) { + deadline = now.getTime() + retry.backoffSettings.totalTimeoutMillis; + } + let retries = 0; + const maxRetries = retry.backoffSettings.maxRetries!; + // TODO: define A/B testing values for retry behaviors. + + /** Repeat the API call as long as necessary. */ + function repeat() { + timeoutId = null; + if (deadline && now.getTime() >= deadline) { + const error = new GoogleError( + 'Retry total timeout exceeded before any response was received'); + error.code = status.DEADLINE_EXCEEDED; + callback(error); + return; + } + + if (retries && retries >= maxRetries) { + const error = new GoogleError( + 'Exceeded maximum number of retries before any ' + + 'response was received'); + error.code = status.DEADLINE_EXCEEDED; + callback(error); + return; + } + + retries++; + const toCall = addTimeoutArg(func, timeout!, otherArgs); + canceller = toCall(argument, (err, response, next, rawResponse) => { + if (!err) { + callback(null, response, next, rawResponse); + return; + } + canceller = null; + if (retry.retryCodes.indexOf(err!.code!) < 0) { + err.note = 'Exception occurred in retry method that was ' + + 'not classified as transient'; + callback(err); + } else { + const toSleep = Math.random() * delay; + timeoutId = setTimeout(() => { + now = new Date(); + delay = Math.min(delay * delayMult, maxDelay); + timeout = Math.min( + timeout! * timeoutMult!, maxTimeout!, deadline - now.getTime()); + repeat(); + }, toSleep); + } + }); + } + + if (maxRetries && deadline!) { + const error = new GoogleError( + 'Cannot set both totalTimeoutMillis and maxRetries ' + + 'in backoffSettings.'); + error.code = status.INVALID_ARGUMENT; + callback(error); + } else { + repeat(); + } + + return { + cancel() { + if (timeoutId) { + clearTimeout(timeoutId); + } + if (canceller) { + canceller.cancel(); + } else { + const error = new GoogleError('cancelled'); + error.code = status.CANCELLED; + callback(error); + } + }, + }; + }; +} diff --git a/src/normalCalls/timeout.ts b/src/normalCalls/timeout.ts new file mode 100644 index 000000000..9cdf6693f --- /dev/null +++ b/src/normalCalls/timeout.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2019 Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {GRPCCall, GRPCCallOtherArgs, SimpleCallbackFunction, UnaryCall} from '../apitypes'; + +/** + * Updates func so that it gets called with the timeout as its final arg. + * + * This converts a function, func, into another function with updated deadline. + * + * @private + * + * @param {GRPCCall} func - a function to be updated. + * @param {number} timeout - to be added to the original function as it final + * positional arg. + * @param {Object} otherArgs - the additional arguments to be passed to func. + * @param {Object=} abTests - the A/B testing key/value pairs. + * @return {function(Object, APICallback)} + * the function with other arguments and the timeout. + */ +export function addTimeoutArg( + 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, callback) => { + 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 UnaryCall)(argument, metadata!, options, callback); + }; +} diff --git a/src/operationsClient.ts b/src/operationsClient.ts index 103b5f353..2cbeede06 100644 --- a/src/operationsClient.ts +++ b/src/operationsClient.ts @@ -1,42 +1,54 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2019 Google LLC + * All rights reserved. * - * 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 + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: * - * http://www.apache.org/licenses/LICENSE-2.0 + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. * - * 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 SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import {GoogleAuth} from 'google-auth-library'; import {ProjectIdCallback} from 'google-auth-library/build/src/auth/googleauth'; import {getProtoPath} from 'google-proto-files'; -import * as apiCallable from './apiCallable'; +import {GaxCall} from './apitypes'; +import {createApiCall} from './createApiCall'; +import {PageDescriptor} from './descriptor'; import * as gax from './gax'; import {ClientStubOptions, GrpcClient} from './grpc'; -import * as pagedIteration from './pagedIteration'; -import * as pathTemplate from './pathTemplate'; const configData = require('./operations_client_config'); -Object.assign(gax, apiCallable); -Object.assign(gax, pathTemplate); -Object.assign(gax, pagedIteration); - export const SERVICE_ADDRESS = 'longrunning.googleapis.com'; +const version = require('../../package.json').version; const DEFAULT_SERVICE_PORT = 443; const CODE_GEN_NAME_VERSION = 'gapic/0.7.1'; const PAGE_DESCRIPTORS = { listOperations: - new gax['PageDescriptor']('pageToken', 'nextPageToken', 'operations'), + new PageDescriptor('pageToken', 'nextPageToken', 'operations'), }; /** @@ -71,10 +83,10 @@ export interface OperationsClientOptions { */ export class OperationsClient { auth: GoogleAuth; - private _getOperation!: Function; - private _listOperations!: Function; - private _cancelOperation!: Function; - private _deleteOperation!: Function; + private _getOperation!: GaxCall; + private _listOperations!: GaxCall; + private _cancelOperation!: GaxCall; + private _deleteOperation!: GaxCall; constructor( // tslint:disable-next-line no-any @@ -92,8 +104,7 @@ export class OperationsClient { googleApiClient.push(opts.libName + '/' + opts.libVersion); } googleApiClient.push( - CODE_GEN_NAME_VERSION, 'gax/' + gax['version'], - 'grpc/' + gaxGrpc.grpcVersion); + CODE_GEN_NAME_VERSION, 'gax/' + version, 'grpc/' + gaxGrpc.grpcVersion); const defaults = gaxGrpc.constructSettings( 'google.longrunning.Operations', configData, opts.clientConfig, @@ -109,7 +120,7 @@ export class OperationsClient { 'deleteOperation', ]; operationsStubMethods.forEach(methodName => { - this['_' + methodName] = gax['createApiCall']( + this['_' + methodName] = createApiCall( operationsStub.then(operationsStub => { return (...args: Array<{}>) => { return operationsStub[methodName].apply(operationsStub, args); @@ -304,7 +315,7 @@ export class OperationsClient { * console.error(err); * }); */ - listOperationsStream(request, options = {}) { + listOperationsStream(request: {}, options: gax.CallSettings) { return PAGE_DESCRIPTORS.listOperations.createStream( this._listOperations, request, options); } diff --git a/src/pagedIteration.ts b/src/pagedIteration.ts deleted file mode 100644 index e7d366ca1..000000000 --- a/src/pagedIteration.ts +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Copyright 2016, Google Inc. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following disclaimer - * in the documentation and/or other materials provided with the - * distribution. - * * Neither the name of Google Inc. nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import * as ended from 'is-stream-ended'; -import {PassThrough, Transform} from 'stream'; - -import {APICall, APICallback, NormalApiCaller, NormalApiCallerSettings} from './apiCallable'; - -export class PagedIteration extends NormalApiCaller { - pageDescriptor: PageDescriptor; - /** - * Creates an API caller that returns a stream to performs page-streaming. - * - * @private - * @constructor - * @param {PageDescriptor} pageDescriptor - indicates the structure - * of page streaming to be performed. - */ - constructor(pageDescriptor: PageDescriptor) { - super(); - this.pageDescriptor = pageDescriptor; - } - - createActualCallback(request: {[index: string]: {}}, callback: APICallback) { - const self = this; - return function fetchNextPageToken( - err: Error|null, response: {[index: string]: {}}) { - if (err) { - callback(err); - return; - } - const resources = response[self.pageDescriptor.resourceField]; - const pageToken = response[self.pageDescriptor.responsePageTokenField]; - if (pageToken) { - request[self.pageDescriptor.requestPageTokenField] = pageToken; - callback(err, resources, request, response); - } else { - callback(err, resources, null, response); - } - }; - } - - wrap(func: Function) { - const self = this; - return function wrappedCall(argument, metadata, options, callback) { - return func( - argument, metadata, options, - self.createActualCallback(argument, callback)); - }; - } - - init(settings: NormalApiCallerSettings, callback: APICallback) { - return super.init(settings, callback); - } - - call(apiCall: APICall, argument: {[index: string]: {}}, settings, canceller) { - argument = Object.assign({}, argument); - if (settings.pageToken) { - argument[this.pageDescriptor.requestPageTokenField] = settings.pageToken; - } - if (settings.pageSize) { - argument[this.pageDescriptor.requestPageSizeField!] = settings.pageSize; - } - if (!settings.autoPaginate) { - NormalApiCaller.prototype.call.call( - this, apiCall, argument, settings, canceller); - return; - } - - const maxResults = settings.maxResults || -1; - const allResources: Array<{}> = []; - function pushResources(err, resources, next) { - if (err) { - canceller.callback(err); - return; - } - - for (let i = 0; i < resources.length; ++i) { - allResources.push(resources[i]); - if (allResources.length === maxResults) { - next = null; - break; - } - } - if (!next) { - canceller.callback(null, allResources); - return; - } - setImmediate(apiCall, next, pushResources); - } - - setImmediate(apiCall, argument, pushResources); - } -} - -export class PageDescriptor { - requestPageTokenField: string; - responsePageTokenField: string; - requestPageSizeField?: string; - resourceField: string; - /** - * Describes the structure of a page-streaming call. - * - * @property {String} requestPageTokenField - * @property {String} responsePageTokenField - * @property {String} resourceField - * - * @param {String} requestPageTokenField - The field name of the page token in - * the request. - * @param {String} responsePageTokenField - The field name of the page token in - * the response. - * @param {String} resourceField - The resource field name. - * - * @constructor - */ - constructor( - requestPageTokenField: string, responsePageTokenField: string, - resourceField: string) { - this.requestPageTokenField = requestPageTokenField; - this.responsePageTokenField = responsePageTokenField; - this.resourceField = resourceField; - } - - /** - * Creates a new object Stream which emits the resource on 'data' event. - * @private - * @param {ApiCall} apiCall - the callable object. - * @param {Object} request - the request object. - * @param {CallOptions=} options - the call options to customize the api call. - * @return {Stream} - a new object Stream. - */ - createStream(apiCall: APICall, request, options): Transform { - const stream = new PassThrough({objectMode: true}); - options = Object.assign({}, options, {autoPaginate: false}); - const maxResults = 'maxResults' in options ? options.maxResults : -1; - let pushCount = 0; - let started = false; - function callback(err: Error|null, resources, next) { - if (err) { - stream.emit('error', err); - return; - } - for (let i = 0; i < resources.length; ++i) { - if (ended(stream)) { - return; - } - if (resources[i] === null) { - continue; - } - stream.push(resources[i]); - pushCount++; - if (pushCount === maxResults) { - stream.end(); - } - } - if (ended(stream)) { - return; - } - if (!next) { - stream.end(); - return; - } - // When pageToken is specified in the original options, it will overwrite - // the page token field in the next request. Therefore it must be cleared. - if ('pageToken' in options) { - delete options.pageToken; - } - if (stream.isPaused()) { - request = next; - started = false; - } else { - setImmediate(apiCall, next, options, callback); - } - } - stream.on('resume', () => { - if (!started) { - started = true; - apiCall(request, options, callback); - } - }); - return stream; - } - - /** - * Returns a new API caller. - * @private - * @return {PageStreamable} - the page streaming caller. - */ - apiCaller() { - return new PagedIteration(this); - } -} diff --git a/src/paginationCalls/pageDescriptor.ts b/src/paginationCalls/pageDescriptor.ts new file mode 100644 index 000000000..a8e1a9e51 --- /dev/null +++ b/src/paginationCalls/pageDescriptor.ts @@ -0,0 +1,122 @@ +/* + * Copyright 2019 Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import * as ended from 'is-stream-ended'; +import {PassThrough, Transform} from 'stream'; + +import {APICaller} from '../apiCaller'; +import {GaxCall} from '../apitypes'; +import {Descriptor} from '../descriptor'; +import {CallSettings} from '../gax'; +import {NormalApiCaller} from '../normalCalls/normalApiCaller'; + +import {PagedApiCaller} from './pagedApiCaller'; + +/** + * A descriptor for methods that support pagination. + */ +export class PageDescriptor implements Descriptor { + requestPageTokenField: string; + responsePageTokenField: string; + requestPageSizeField?: string; + resourceField: string; + + constructor( + requestPageTokenField: string, responsePageTokenField: string, + resourceField: string) { + this.requestPageTokenField = requestPageTokenField; + this.responsePageTokenField = responsePageTokenField; + this.resourceField = resourceField; + } + + /** + * Creates a new object Stream which emits the resource on 'data' event. + */ + createStream(apiCall: GaxCall, request: {}, options: CallSettings): + Transform { + const stream = new PassThrough({objectMode: true}); + options = Object.assign({}, options, {autoPaginate: false}); + const maxResults = 'maxResults' in options ? options.maxResults : -1; + let pushCount = 0; + let started = false; + function callback(err: Error|null, resources, next) { + if (err) { + stream.emit('error', err); + return; + } + for (let i = 0; i < resources.length; ++i) { + if (ended(stream)) { + return; + } + if (resources[i] === null) { + continue; + } + stream.push(resources[i]); + pushCount++; + if (pushCount === maxResults) { + stream.end(); + } + } + if (ended(stream)) { + return; + } + if (!next) { + stream.end(); + return; + } + // When pageToken is specified in the original options, it will overwrite + // the page token field in the next request. Therefore it must be cleared. + if ('pageToken' in options) { + delete options.pageToken; + } + if (stream.isPaused()) { + request = next; + started = false; + } else { + setImmediate(apiCall, next, options, callback); + } + } + stream.on('resume', () => { + if (!started) { + started = true; + apiCall(request, options, callback); + } + }); + return stream; + } + + getApiCaller(settings: CallSettings): APICaller { + if (!settings.autoPaginate) { + return new NormalApiCaller(); + } + return new PagedApiCaller(this); + } +} diff --git a/src/paginationCalls/pagedApiCaller.ts b/src/paginationCalls/pagedApiCaller.ts new file mode 100644 index 000000000..ff581d8f8 --- /dev/null +++ b/src/paginationCalls/pagedApiCaller.ts @@ -0,0 +1,144 @@ +/* + * Copyright 2019, Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {APICaller, ApiCallerSettings} from '../apiCaller'; +import {GaxCall, GRPCCall, NextPageRequestType, SimpleCallbackFunction, UnaryCall} from '../apitypes'; +import {APICallback} from '../apitypes'; +import {OngoingCall, OngoingCallPromise} from '../call'; +import {CallOptions} from '../gax'; +import {GoogleError} from '../googleError'; + +import {PageDescriptor} from './pageDescriptor'; + +export class PagedApiCaller implements APICaller { + pageDescriptor: PageDescriptor; + /** + * Creates an API caller that returns a stream to performs page-streaming. + * + * @private + * @constructor + * @param {PageDescriptor} pageDescriptor - indicates the structure + * of page streaming to be performed. + */ + constructor(pageDescriptor: PageDescriptor) { + this.pageDescriptor = pageDescriptor; + } + + private createActualCallback( + request: NextPageRequestType, callback: APICallback): APICallback { + const self = this; + return function fetchNextPageToken( + err: Error|null, response: NextPageRequestType|undefined) { + if (err) { + callback(err); + return; + } + if (!response) { + callback(new GoogleError( + 'Undefined response in pagination method callback.')); + return; + } + const resources = response[self.pageDescriptor.resourceField]; + const pageToken = response[self.pageDescriptor.responsePageTokenField]; + if (pageToken) { + request![self.pageDescriptor.requestPageTokenField] = pageToken; + callback(err, resources, request, response); + } else { + callback(err, resources, null, response); + } + }; + } + + wrap(func: GRPCCall): GRPCCall { + const self = this; + return function wrappedCall(argument, metadata, options, callback) { + return (func as UnaryCall)( + argument, metadata, options, + self.createActualCallback(argument, callback)); + }; + } + + init(settings: ApiCallerSettings, callback?: APICallback) { + if (callback) { + return new OngoingCall(callback); + } + return new OngoingCallPromise(settings.promise); + } + + call( + apiCall: SimpleCallbackFunction, argument: {[index: string]: {}}, + settings: CallOptions, canceller: OngoingCall) { + argument = Object.assign({}, argument); + if (settings.pageToken) { + argument[this.pageDescriptor.requestPageTokenField] = settings.pageToken; + } + if (settings.pageSize) { + argument[this.pageDescriptor.requestPageSizeField!] = settings.pageSize; + } + if (!settings.autoPaginate) { + // they don't want auto-pagination this time - okay, just call once + canceller.call(apiCall, argument); + return; + } + + const maxResults = settings.maxResults || -1; + const allResources: Array<{}> = []; + function pushResources(err, resources, next) { + if (err) { + canceller.callback!(err); + return; + } + + for (let i = 0; i < resources.length; ++i) { + allResources.push(resources[i]); + if (allResources.length === maxResults) { + next = null; + break; + } + } + if (!next) { + canceller.callback!(null, allResources); + return; + } + setImmediate(apiCall, next, pushResources); + } + + setImmediate(apiCall, argument, pushResources); + } + + fail(canceller: OngoingCallPromise, err: GoogleError): void { + canceller.callback!(err); + } + + result(canceller: OngoingCallPromise) { + return canceller.promise; + } +} diff --git a/src/parserExtras.ts b/src/parserExtras.ts index a8252ea83..f0d6d05b8 100644 --- a/src/parserExtras.ts +++ b/src/parserExtras.ts @@ -1,6 +1,5 @@ /* - * - * Copyright 2016, Google Inc. + * Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,7 +27,6 @@ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * */ import * as util from 'util'; diff --git a/src/pathTemplate.ts b/src/pathTemplate.ts index f1b86287c..a196d1f38 100644 --- a/src/pathTemplate.ts +++ b/src/pathTemplate.ts @@ -1,6 +1,5 @@ /* - * - * Copyright 2016, Google Inc. + * Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,7 +27,6 @@ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * */ /* diff --git a/src/pathTemplateParser.pegjs b/src/pathTemplateParser.pegjs index c92d23bf0..ec921a1f6 100644 --- a/src/pathTemplateParser.pegjs +++ b/src/pathTemplateParser.pegjs @@ -1,6 +1,5 @@ /* - * - * Copyright 2016, Google Inc. + * Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,7 +27,6 @@ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * */ { diff --git a/src/routingHeader.ts b/src/routingHeader.ts index e7af663c4..d42ae03e6 100644 --- a/src/routingHeader.ts +++ b/src/routingHeader.ts @@ -1,5 +1,5 @@ /* - * Copyright 2017, Google Inc. + * Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/streamingCalls/streamDescriptor.ts b/src/streamingCalls/streamDescriptor.ts new file mode 100644 index 000000000..c36ad823a --- /dev/null +++ b/src/streamingCalls/streamDescriptor.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2019 Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {APICaller} from '../apiCaller'; +import {Descriptor} from '../descriptor'; +import {CallSettings} from '../gax'; + +import {StreamType} from './streaming'; +import {StreamingApiCaller} from './streamingApiCaller'; + +/** + * A descriptor for streaming calls. + */ +export class StreamDescriptor implements Descriptor { + type: StreamType; + + constructor(streamType: StreamType) { + this.type = streamType; + } + + 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/streaming.ts b/src/streamingCalls/streaming.ts similarity index 70% rename from src/streaming.ts rename to src/streamingCalls/streaming.ts index 1ed0dd29b..05a060ed0 100644 --- a/src/streaming.ts +++ b/src/streamingCalls/streaming.ts @@ -1,5 +1,5 @@ /** - * Copyright 2016, Google Inc. + * Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,8 +33,7 @@ import {Duplex, DuplexOptions, Readable, Stream, Writable} from 'stream'; -import {APICall, APICallback} from './apiCallable'; -import {warn} from './warnings'; +import {APICallback, CancellableStream, GRPCCallResult, SimpleCallbackFunction} from '../apitypes'; const duplexify: DuplexifyConstructor = require('duplexify'); const retryRequest = require('retry-request'); @@ -75,11 +74,11 @@ export enum StreamType { BIDI_STREAMING = 3, } -export class StreamProxy extends duplexify { +export class StreamProxy extends duplexify implements GRPCCallResult { type: StreamType; - private _callback?: Function; + private _callback: APICallback; private _isCancelCalled: boolean; - stream?: Duplex&{cancel: () => void}; + stream?: CancellableStream; /** * StreamProxy is a proxy to gRPC-streaming method. * @@ -141,7 +140,7 @@ export class StreamProxy extends duplexify { * @param {ApiCall} apiCall - the API function to be called. * @param {Object} argument - the argument to be passed to the apiCall. */ - setStream(apiCall: APICall, argument: {}) { + setStream(apiCall: SimpleCallbackFunction, argument: {}) { if (this.type === StreamType.SERVER_STREAMING) { const retryStream = retryRequest(null, { objectMode: true, @@ -152,7 +151,7 @@ export class StreamProxy extends duplexify { } return; } - const stream = apiCall(argument, this._callback); + const stream = apiCall(argument, this._callback) as CancellableStream; this.stream = stream; this.forwardEvents(stream); return stream; @@ -162,7 +161,7 @@ export class StreamProxy extends duplexify { return; } - const stream = apiCall(argument, this._callback); + const stream = apiCall(argument, this._callback) as CancellableStream; this.stream = stream; this.forwardEvents(stream); @@ -180,76 +179,3 @@ export class StreamProxy extends duplexify { } } } - -export class GrpcStreamable { - descriptor: StreamDescriptor; - - /** - * An API caller for methods of gRPC streaming. - * @private - * @constructor - * @param {StreamDescriptor} descriptor - the descriptor of the method structure. - */ - constructor(descriptor: StreamDescriptor) { - this.descriptor = descriptor; - } - - init(settings: {}, callback: APICallback): StreamProxy { - return new StreamProxy(this.descriptor.type, callback); - } - - wrap(func: Function) { - switch (this.descriptor.type) { - case StreamType.SERVER_STREAMING: - return (argument: {}, metadata: {}, options: {}) => { - return func(argument, metadata, options); - }; - case StreamType.CLIENT_STREAMING: - return (argument: {}, metadata: {}, options: {}, callback: {}) => { - return func(metadata, options, callback); - }; - case StreamType.BIDI_STREAMING: - return (argument: {}, metadata: {}, options: {}) => { - return func(metadata, options); - }; - default: - warn( - 'streaming_wrap_unknown_stream_type', - `Unknown stream type: ${this.descriptor.type}`); - } - return func; - } - - call(apiCall: APICall, argument: {}, settings: {}, stream: StreamProxy) { - stream.setStream(apiCall, argument); - } - - fail(stream: Stream, err: Error) { - stream.emit('error', err); - } - - result(stream: Stream) { - return stream; - } -} - -export class StreamDescriptor { - type: StreamType; - /** - * Describes the structure of gRPC streaming call. - * @constructor - * @param {StreamType} streamType - the type of streaming. - */ - constructor(streamType: StreamType) { - this.type = streamType; - } - - apiCaller(settings: {retry: null}): GrpcStreamable { - // 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 GrpcStreamable(this); - } -} diff --git a/src/streamingCalls/streamingApiCaller.ts b/src/streamingCalls/streamingApiCaller.ts new file mode 100644 index 000000000..dedbe7b44 --- /dev/null +++ b/src/streamingCalls/streamingApiCaller.ts @@ -0,0 +1,93 @@ +/* + * Copyright 2019, Google LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +import {APICaller, ApiCallerSettings} from '../apiCaller'; +import {APICallback, BiDiStreamingCall, CancellableStream, ClientStreamingCall, GRPCCall, ServerStreamingCall, SimpleCallbackFunction} from '../apitypes'; +import {warn} from '../warnings'; + +import {StreamDescriptor} from './streamDescriptor'; +import {StreamProxy, StreamType} from './streaming'; + +export class StreamingApiCaller implements APICaller { + descriptor: StreamDescriptor; + + /** + * An API caller for methods of gRPC streaming. + * @private + * @constructor + * @param {StreamDescriptor} descriptor - the descriptor of the method structure. + */ + constructor(descriptor: StreamDescriptor) { + this.descriptor = descriptor; + } + + init(settings: ApiCallerSettings, callback: APICallback): StreamProxy { + return new StreamProxy(this.descriptor.type, callback); + } + + wrap(func: GRPCCall): GRPCCall { + switch (this.descriptor.type) { + case StreamType.SERVER_STREAMING: + return (argument: {}, metadata: {}, options: {}) => { + return (func as ServerStreamingCall)(argument, metadata, options); + }; + case StreamType.CLIENT_STREAMING: + return (argument: {}, metadata: {}, options: {}, + callback?: APICallback) => { + return (func as ClientStreamingCall)(metadata, options, callback); + }; + case StreamType.BIDI_STREAMING: + return (argument: {}, metadata: {}, options: {}) => { + return (func as BiDiStreamingCall)(metadata, options); + }; + default: + warn( + 'streaming_wrap_unknown_stream_type', + `Unknown stream type: ${this.descriptor.type}`); + } + return func; + } + + call( + apiCall: SimpleCallbackFunction, argument: {}, settings: {}, + stream: StreamProxy) { + stream.setStream(apiCall, argument); + } + + fail(stream: CancellableStream, err: Error) { + stream.emit('error', err); + } + + result(stream: CancellableStream) { + return stream; + } +} diff --git a/src/warnings.ts b/src/warnings.ts index bedf59fcf..b0fa1c387 100644 --- a/src/warnings.ts +++ b/src/warnings.ts @@ -1,17 +1,32 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. +/* + * Copyright 2019 Google LLC + * All rights reserved. * - * 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 + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: * - * http://www.apache.org/licenses/LICENSE-2.0 + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. * - * 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 SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import * as semver from 'semver'; diff --git a/system-test/system.ts b/system-test/system.ts index 7250f0798..8be5a4ae9 100644 --- a/system-test/system.ts +++ b/system-test/system.ts @@ -1,17 +1,32 @@ -/** - * Copyright 2018 Google LLC. All Rights Reserved. +/* + * Copyright 2019 Google LLC + * All rights reserved. * - * 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 + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: * - * http://www.apache.org/licenses/LICENSE-2.0 + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. * - * 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 SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import * as cp from 'child_process'; diff --git a/test/apiCallable.ts b/test/apiCallable.ts index a11b3b11d..91c65916d 100644 --- a/test/apiCallable.ts +++ b/test/apiCallable.ts @@ -1,4 +1,4 @@ -/* Copyright 2016, Google Inc. +/* Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,7 +33,7 @@ import {status} from 'grpc'; import * as sinon from 'sinon'; import * as gax from '../src/gax'; -import {GoogleError} from '../src/GoogleError'; +import {GoogleError} from '../src/googleError'; import * as utils from './utils'; @@ -52,7 +52,7 @@ describe('createApiCall', () => { callback(null, argument); } const apiCall = createApiCall(func); - apiCall(42, null, (err, resp) => { + apiCall(42, undefined, (err, resp) => { expect(resp).to.eq(42); // tslint:disable-next-line no-unused-expression expect(deadlineArg).to.be.ok; @@ -65,7 +65,7 @@ describe('createApiCall', () => { callback(null, options.deadline.getTime()); } const apiCall = createApiCall(func, {settings: {timeout: 100}}); - apiCall(null, {timeout: 200}, (err, resp) => { + apiCall({}, {timeout: 200}, (err, resp) => { const now = new Date(); const originalDeadline = now.getTime() + 100; const expectedDeadline = now.getTime() + 200; @@ -93,7 +93,7 @@ describe('createApiCall', () => { }); const start = new Date().getTime(); - apiCall(null, null, (err, resp) => { + apiCall({}, undefined, (err, resp) => { // The verifying value is slightly bigger than the expected number // 2000 / 30000, because sometimes runtime can consume some time before // the call starts. @@ -126,7 +126,7 @@ describe('Promise', () => { it('emits error on failure', (done) => { const apiCall = createApiCall(fail); - apiCall(null, null) + apiCall({}, undefined) .then(() => { done(new Error('should not reach')); }) @@ -204,7 +204,7 @@ describe('Promise', () => { callback(null, 42); } const apiCall = createApiCall(func); - expect(apiCall(null, null, (err, response) => { + expect(apiCall({}, undefined, (err, response) => { // tslint:disable-next-line no-unused-expression expect(err).to.be.null; expect(response).to.eq(42); @@ -223,7 +223,8 @@ describe('Promise', () => { callback(null, 42); } const apiCall = createApiCall(func); - apiCall(null, {promise: MockPromise}) + // @ts-ignore incomplete options + apiCall({}, {promise: MockPromise}) .then(response => { expect(response).to.be.an('array'); expect(response[0]).to.eq(42); @@ -252,7 +253,7 @@ describe('retryable', () => { callback(null, 1729); } const apiCall = createApiCall(func, settings); - apiCall(null, null, (err, resp) => { + apiCall({}, undefined, (err, resp) => { expect(resp).to.eq(1729); expect(toAttempt).to.eq(0); // tslint:disable-next-line no-unused-expression @@ -274,7 +275,7 @@ describe('retryable', () => { callback(null, 1729); } const apiCall = createApiCall(func, settings); - apiCall(null, null) + apiCall({}, undefined) .then(resp => { expect(resp).to.be.an('array'); expect(resp[0]).to.eq(1729); @@ -303,7 +304,7 @@ describe('retryable', () => { }, 10); } const apiCall = createApiCall(func, settings); - promise = apiCall(null, null); + promise = apiCall({}, undefined); promise .then(() => { done(new Error('should not reach')); @@ -320,7 +321,7 @@ describe('retryable', () => { const settings = {settings: {timeout: 0, retry: retryOptions}}; const spy = sinon.spy(fail); const apiCall = createApiCall(spy, settings); - apiCall(null, null, err => { + apiCall({}, undefined, err => { expect(err).to.be.an('error'); expect(err!.code).to.eq(FAKE_STATUS_CODE_1); // tslint:disable-next-line no-unused-expression @@ -332,7 +333,7 @@ describe('retryable', () => { it('aborts retries', (done) => { const apiCall = createApiCall(fail, settings); - apiCall(null, null, err => { + apiCall({}, undefined, err => { expect(err).to.be.instanceOf(GoogleError); expect(err!.code).to.equal(status.DEADLINE_EXCEEDED); done(); @@ -343,7 +344,7 @@ describe('retryable', () => { const toAttempt = 3; const spy = sinon.spy(fail); const apiCall = createApiCall(spy, settings); - apiCall(null, null, err => { + apiCall({}, undefined, err => { expect(err).to.be.an('error'); expect(err!.code).to.eq(FAKE_STATUS_CODE_1); // tslint:disable-next-line no-unused-expression @@ -365,7 +366,7 @@ describe('retryable', () => { }; const spy = sinon.spy(fail); const apiCall = createApiCall(spy, maxRetrySettings); - apiCall(null, null, err => { + apiCall({}, undefined, err => { expect(err).to.be.instanceOf(GoogleError); expect(err!.code).to.equal(status.DEADLINE_EXCEEDED); expect(spy.callCount).to.eq(toAttempt); @@ -385,7 +386,7 @@ describe('retryable', () => { }; const spy = sinon.spy(fail); const apiCall = createApiCall(spy, maxRetrySettings); - apiCall(null, null, err => { + apiCall({}, undefined, err => { expect(err).to.be.instanceOf(GoogleError); expect(err!.code).to.equal(status.INVALID_ARGUMENT); expect(spy.callCount).to.eq(0); @@ -401,7 +402,7 @@ describe('retryable', () => { } const spy = sinon.spy(func); const apiCall = createApiCall(spy, settings); - apiCall(null, null, err => { + apiCall({}, undefined, err => { expect(err).to.be.an('error'); expect(err!.code).to.eq(FAKE_STATUS_CODE_2); // tslint:disable-next-line no-unused-expression @@ -416,7 +417,7 @@ describe('retryable', () => { callback(null, null); } const apiCall = createApiCall(func, settings); - apiCall(null, null, (err, resp) => { + apiCall({}, undefined, (err, resp) => { // tslint:disable-next-line no-unused-expression expect(err).to.be.null; // tslint:disable-next-line no-unused-expression @@ -435,7 +436,7 @@ describe('retryable', () => { settings: {timeout: 0, retry: retryOptions}, }); - apiCall(null, null, err => { + apiCall({}, undefined, err => { expect(err).to.be.an('error'); expect(err!.code).to.eq(FAKE_STATUS_CODE_1); // tslint:disable-next-line no-unused-expression @@ -467,14 +468,14 @@ describe('retryable', () => { }; const apiCall = createApiCall(func, settings); mockBuilder.withExactArgs({retry: '2'}); - return apiCall(null, null) + return apiCall({}, undefined) .then(() => { mockBuilder.verify(); mockBuilder.reset(); const backoff = gax.createMaxRetriesBackoffSettings(0, 0, 0, 0, 0, 0, 5); mockBuilder.withExactArgs({retry: '1'}); - return apiCall(null, {retry: utils.createRetryOptions(backoff)}); + return apiCall({}, {retry: utils.createRetryOptions(backoff)}); }) .then(() => { mockBuilder.verify(); @@ -483,7 +484,7 @@ describe('retryable', () => { const options = { retry: utils.createRetryOptions(0, 0, 0, 0, 0, 0, 200), }; - return apiCall(null, options); + return apiCall({}, options); }) .then(() => { mockBuilder.verify(); @@ -492,7 +493,7 @@ describe('retryable', () => { it('forwards metadata to builder', (done) => { function func(argument, metadata, options, callback) { - callback(); + callback(null, {}); } let gotHeaders; @@ -509,7 +510,7 @@ describe('retryable', () => { h1: 'val1', h2: 'val2', }; - apiCall(null, {otherArgs: {headers}}).then(() => { + apiCall({}, {otherArgs: {headers}}).then(() => { expect(gotHeaders.h1).to.deep.equal('val1'); expect(gotHeaders.h2).to.deep.equal('val2'); done(); diff --git a/test/bundling.ts b/test/bundling.ts index 16e0513c1..7b57b7779 100644 --- a/test/bundling.ts +++ b/test/bundling.ts @@ -1,4 +1,4 @@ -/* Copyright 2016, Google Inc. +/* Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -32,8 +32,11 @@ import {expect} from 'chai'; import {status} from 'grpc'; import * as sinon from 'sinon'; -import * as bundling from '../src/bundling'; -import {GoogleError} from '../src/GoogleError'; +import {BundleDescriptor} from '../src/bundlingCalls/bundleDescriptor'; +import {BundleExecutor} from '../src/bundlingCalls/bundleExecutor'; +import {computeBundleId} from '../src/bundlingCalls/bundlingUtils'; +import {deepCopyForResponse, Task} from '../src/bundlingCalls/task'; +import {GoogleError} from '../src/googleError'; import {createApiCall} from './utils'; @@ -108,7 +111,7 @@ describe('computeBundleId', () => { ]; testCases.forEach(t => { it(t.message, () => { - expect(bundling.computeBundleId(t.object, t.fields)).to.equal(t.want); + expect(computeBundleId(t.object, t.fields)).to.equal(t.want); }); }); }); @@ -134,7 +137,7 @@ describe('computeBundleId', () => { testCases.forEach(t => { it(t.message, () => { // tslint:disable-next-line no-unused-expression - expect(bundling.computeBundleId(t.object, t.fields)).to.be.undefined; + expect(computeBundleId(t.object, t.fields)).to.be.undefined; }); }); }); @@ -143,7 +146,7 @@ describe('computeBundleId', () => { describe('deepCopyForResponse', () => { it('copies deeply', () => { const input = {foo: {bar: [1, 2]}}; - const output = bundling.deepCopyForResponse(input, null); + const output = deepCopyForResponse(input, null); expect(output).to.deep.equal(input); expect(output.foo).to.not.equal(input.foo); expect(output.foo.bar).to.not.equal(input.foo.bar); @@ -151,7 +154,7 @@ describe('deepCopyForResponse', () => { it('respects subresponseInfo', () => { const input = {foo: [1, 2, 3, 4], bar: {foo: [1, 2, 3, 4]}}; - const output = bundling.deepCopyForResponse(input, { + const output = deepCopyForResponse(input, { field: 'foo', start: 0, end: 2, @@ -159,7 +162,7 @@ describe('deepCopyForResponse', () => { expect(output).to.deep.equal({foo: [1, 2], bar: {foo: [1, 2, 3, 4]}}); expect(output.bar).to.not.equal(input.bar); - const output2 = bundling.deepCopyForResponse(input, { + const output2 = deepCopyForResponse(input, { field: 'foo', start: 2, end: 4, @@ -186,7 +189,7 @@ describe('deepCopyForResponse', () => { foo: 1, }, }; - const output = bundling.deepCopyForResponse(input, null); + const output = deepCopyForResponse(input, null); expect(output).to.deep.equal(input); expect(output.copyable).to.not.equal(input.copyable); expect(output.arraybuffer).to.not.equal(input.arraybuffer); @@ -195,7 +198,7 @@ describe('deepCopyForResponse', () => { it('ignores erroneous subresponseInfo', () => { const input = {foo: 1, bar: {foo: [1, 2, 3, 4]}}; - const output = bundling.deepCopyForResponse(input, { + const output = deepCopyForResponse(input, { field: 'foo', start: 0, end: 2, @@ -206,7 +209,7 @@ describe('deepCopyForResponse', () => { describe('Task', () => { function testTask(apiCall?) { - return new bundling.Task(apiCall, {}, 'field1', null); + return new Task(apiCall, {}, 'field1', null); } let id = 0; @@ -367,7 +370,7 @@ describe('Task', () => { const callback = sinon.spy((e, data) => { expect(e).to.equal(err); // tslint:disable-next-line no-unused-expression - expect(data).to.be.null; + expect(data).to.be.undefined; if (callback.callCount === t.data.length) { expect(apiCall.callCount).to.eq(1); done(); @@ -486,15 +489,17 @@ describe('Task', () => { describe('Executor', () => { function apiCall(request, callback) { callback(null, request); + return {cancel: () => {}}; } function failing(request, callback) { callback(new Error('failure')); + return {cancel: () => {}}; } function newExecutor(options) { - const descriptor = new bundling.BundleDescriptor( - 'field1', ['field2'], 'field1', byteLength); - return new bundling.BundleExecutor(options, descriptor); + const descriptor = + new BundleDescriptor('field1', ['field2'], 'field1', byteLength); + return new BundleExecutor(options, descriptor); } it('groups api calls by the id', () => { @@ -546,7 +551,8 @@ describe('Executor', () => { done(); } executor.schedule(spy, {field1: [1, 2], field2: 'id1'}, (err, resp) => { - expect(resp!.field1).to.deep.eq([1, 2]); + // @ts-ignore unknown field + expect(resp.field1).to.deep.eq([1, 2]); expect(unbundledCallCounter).to.eq(2); counter++; if (counter === 4) { @@ -554,11 +560,13 @@ describe('Executor', () => { } }); executor.schedule(spy, {field1: [3]}, (err, resp) => { + // @ts-ignore unknown field expect(resp.field1).to.deep.eq([3]); unbundledCallCounter++; counter++; }); executor.schedule(spy, {field1: [4], field2: 'id1'}, (err, resp) => { + // @ts-ignore unknown field expect(resp.field1).to.deep.eq([4]); expect(unbundledCallCounter).to.eq(2); counter++; @@ -567,6 +575,7 @@ describe('Executor', () => { } }); executor.schedule(spy, {field1: [5, 6]}, (err, resp) => { + // @ts-ignore unknown field expect(resp.field1).to.deep.eq([5, 6]); unbundledCallCounter++; counter++; @@ -606,6 +615,7 @@ describe('Executor', () => { executor.schedule( spyApi, {field1: [3, 4], field2: 'id'}, (err, resp) => { + // @ts-ignore unknown field expect(resp.field1).to.deep.equal([3, 4]); expect(spyApi.callCount).to.eq(1); done(); @@ -617,6 +627,7 @@ describe('Executor', () => { it('distinguishes a running task and a scheduled one', (done) => { let counter = 0; + // @ts-ignore cancellation logic is broken here executor.schedule(timedAPI, {field1: [1, 2], field2: 'id'}, err => { // tslint:disable-next-line no-unused-expression expect(err).to.be.null; @@ -629,6 +640,7 @@ describe('Executor', () => { executor._runNow('id'); const canceller = + // @ts-ignore cancellation logic is broken here executor.schedule(timedAPI, {field1: [1, 2], field2: 'id'}, err => { expect(err).to.be.an.instanceOf(GoogleError); expect(err!.code).to.equal(status.CANCELLED); @@ -644,6 +656,7 @@ describe('Executor', () => { const spy = sinon.spy((request, callback) => { expect(request.field1.length).to.eq(threshold); callback(null, request); + return {cancel: () => {}}; }); for (let i = 0; i < threshold - 1; ++i) { executor.schedule(spy, {field1: [1], field2: 'id1'}); @@ -670,6 +683,7 @@ describe('Executor', () => { expect(request.field1.length).to.eq(count); expect(byteLength(request.field1)).to.be.least(threshold); callback(null, request); + return {cancel: () => {}}; }); for (let i = 0; i < count - 1; ++i) { executor.schedule(spy, {field1: [1], field2: 'id1'}); @@ -696,6 +710,7 @@ describe('Executor', () => { const spy = sinon.spy((request, callback) => { expect(request.field1).to.be.an.instanceOf(Array); callback(null, request); + return {cancel: () => {}}; }); executor.schedule(spy, {field1: [1, 2], field2: 'id'}); executor.schedule(spy, {field1: [3, 4], field2: 'id'}); @@ -729,6 +744,7 @@ describe('Executor', () => { const spy = sinon.spy((request, callback) => { expect(request.field1).to.be.an.instanceOf(Array); callback(null, request); + return {cancel: () => {}}; }); executor.schedule(spy, {field1: [1, 2], field2: 'id'}); executor.schedule(spy, {field1: [3, 4], field2: 'id'}); @@ -761,6 +777,7 @@ describe('Executor', () => { const spy = sinon.spy((request, callback) => { expect(request.field1.length).to.eq(threshold); callback(null, request); + return {cancel: () => {}}; }); executor.schedule(spy, {field1: [1, 2], field2: 'id1'}); setTimeout(() => { @@ -819,7 +836,7 @@ describe('bundleable', () => { } const bundleOptions = {elementCountThreshold: 12, delayThreshold: 10}; const descriptor = - new bundling.BundleDescriptor('field1', ['field2'], 'field1', byteLength); + new BundleDescriptor('field1', ['field2'], 'field1', byteLength); const settings = { settings: {bundleOptions}, descriptor, @@ -836,14 +853,16 @@ describe('bundleable', () => { } }); const apiCall = createApiCall(spy, settings); - apiCall({field1: [1, 2, 3], field2: 'id'}, null, (err, obj) => { + apiCall({field1: [1, 2, 3], field2: 'id'}, undefined, (err, obj) => { if (err) { done(err); } else { callback([obj]); } }); - apiCall({field1: [1, 2, 3], field2: 'id'}, null).then(callback).catch(done); + apiCall({field1: [1, 2, 3], field2: 'id'}, undefined) + .then(callback) + .catch(done); }); it('does not fail if bundle field is not set', (done) => { @@ -865,8 +884,8 @@ describe('bundleable', () => { warnStub.restore(); done(err); } - apiCall({field2: 'id1'}, null).then(callback, error); - apiCall({field2: 'id2'}, null).then(callback, error); + apiCall({field2: 'id1'}, undefined).then(callback, error); + apiCall({field2: 'id2'}, undefined).then(callback, error); }); it('suppresses bundling behavior by call options', (done) => { @@ -888,13 +907,13 @@ describe('bundleable', () => { expect(obj[0].field1).to.deep.equal([1, 2, 3]); } const apiCall = createApiCall(spy, settings); - apiCall({field1: [1, 2, 3], field2: 'id'}, null) + apiCall({field1: [1, 2, 3], field2: 'id'}, undefined) .then(bundledCallback) .catch(done); apiCall({field1: [1, 2, 3], field2: 'id'}, {isBundling: false}) .then(unbundledCallback) .catch(done); - apiCall({field1: [1, 2, 3], field2: 'id'}, null) + apiCall({field1: [1, 2, 3], field2: 'id'}, undefined) .then(bundledCallback) .catch(done); }); @@ -903,9 +922,10 @@ describe('bundleable', () => { const apiCall = createApiCall(func, settings); let expectedSuccess = false; let expectedFailure = false; - apiCall({field1: [1, 2, 3], field2: 'id'}, null) + apiCall({field1: [1, 2, 3], field2: 'id'}, undefined) .then(obj => { expect(obj).to.be.an('array'); + // @ts-ignore response type expect(obj[0].field1).to.deep.equal([1, 2, 3]); expectedSuccess = true; if (expectedSuccess && expectedFailure) { @@ -913,7 +933,7 @@ describe('bundleable', () => { } }) .catch(done); - const p = apiCall({field1: [1, 2, 3], field2: 'id'}, null); + const p = apiCall({field1: [1, 2, 3], field2: 'id'}, undefined); p.then(() => { done(new Error('should not succeed')); }).catch(err => { diff --git a/test/gax.ts b/test/gax.ts index 76a577e73..0b0ac1b82 100644 --- a/test/gax.ts +++ b/test/gax.ts @@ -1,4 +1,4 @@ -/* Copyright 2016, Google Inc. +/* Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/test/grpc.ts b/test/grpc.ts index 2580da681..9aea18a37 100644 --- a/test/grpc.ts +++ b/test/grpc.ts @@ -1,4 +1,4 @@ -/* Copyright 2017, Google Inc. +/* Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/test/longrunning.ts b/test/longrunning.ts index 9c453542d..a08421570 100644 --- a/test/longrunning.ts +++ b/test/longrunning.ts @@ -1,4 +1,4 @@ -/* Copyright 2016, Google Inc. +/* Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -32,9 +32,11 @@ import {expect} from 'chai'; import {status} from 'grpc'; import * as sinon from 'sinon'; +import {LongrunningDescriptor} from '../src'; +import {GaxCallPromise} from '../src/apitypes'; import * as gax from '../src/gax'; -import {GoogleError} from '../src/GoogleError'; -import * as longrunning from '../src/longrunning'; +import {GoogleError} from '../src/googleError'; +import * as longrunning from '../src/longRunningCalls/longrunning'; import {OperationsClient} from '../src/operationsClient'; import * as utils from './utils'; @@ -92,8 +94,8 @@ const mockDecoder = val => { function createApiCall(func, client?) { const descriptor = - new longrunning.LongrunningDescriptor(client, mockDecoder, mockDecoder); - return utils.createApiCall(func, {descriptor}); + new LongrunningDescriptor(client, mockDecoder, mockDecoder); + return utils.createApiCall(func, {descriptor}) as GaxCallPromise; } interface SpyableOperationsClient extends OperationsClient { @@ -144,9 +146,9 @@ describe('longrunning', () => { const defaultMaxRetryDelayMillis = 60000; const defaultTotalTimeoutMillis = null; const apiCall = createApiCall(func); - apiCall() + apiCall({}) .then(responses => { - const operation = responses[0]; + const operation = responses[0] as longrunning.Operation; const rawResponse = responses[1]; expect(operation).to.be.an('object'); expect(operation).to.have.property('backoffSettings'); @@ -173,7 +175,7 @@ describe('longrunning', () => { describe('operation', () => { it('returns an Operation with correct values', done => { const client = mockOperationsClient(); - const desc = new longrunning.LongrunningDescriptor( + const desc = new LongrunningDescriptor( client as OperationsClient, mockDecoder, mockDecoder); const initialRetryDelayMillis = 1; const retryDelayMultiplier = 2; @@ -211,9 +213,9 @@ describe('longrunning', () => { }; const client = mockOperationsClient(); const apiCall = createApiCall(func, client); - apiCall() + apiCall({}) .then(responses => { - const operation = responses[0]; + const operation = responses[0] as longrunning.Operation; operation.getOperation((err, result, metadata, rawResponse) => { if (err) { done(err); @@ -234,9 +236,9 @@ describe('longrunning', () => { }; const client = mockOperationsClient(); const apiCall = createApiCall(func, client); - apiCall() + apiCall({}) .then(responses => { - const operation = responses[0]; + const operation = responses[0] as longrunning.Operation; operation.getOperation((err, result, metadata, rawResponse) => { if (err) { done(err); @@ -259,9 +261,9 @@ describe('longrunning', () => { }; const client = mockOperationsClient(); const apiCall = createApiCall(func, client); - apiCall() + apiCall({}) .then(responses => { - const operation = responses[0]; + const operation = responses[0] as longrunning.Operation; expect( operation.getOperation( (err, result, metadata, rawResponse) => { @@ -289,9 +291,9 @@ describe('longrunning', () => { }; const client = mockOperationsClient(); const apiCall = createApiCall(func, client); - apiCall() + apiCall({}) .then(responses => { - const operation = responses[0]; + const operation = responses[0] as longrunning.Operation; return operation.getOperation(); }) .then(responses => { @@ -316,9 +318,9 @@ describe('longrunning', () => { }; const client = mockOperationsClient(); const apiCall = createApiCall(func, client); - apiCall() + apiCall({}) .then(responses => { - const operation = responses[0]; + const operation = responses[0] as longrunning.Operation; return operation.getOperation(); }) .then(() => { @@ -344,7 +346,8 @@ describe('longrunning', () => { const client = mockOperationsClient(); const apiCall = createApiCall(func, client); - apiCall(null, {promise: MockPromise}).then(responses => { + // @ts-ignore incomplete options + apiCall({}, {promise: MockPromise}).then(responses => { const operation = responses[0]; operation.getOperation(); // tslint:disable-next-line no-unused-expression @@ -363,9 +366,9 @@ describe('longrunning', () => { const client = mockOperationsClient({expectedCalls}); const apiCall = createApiCall(func, client); - apiCall() + apiCall({}) .then(responses => { - const operation = responses[0]; + const operation = responses[0] as longrunning.Operation; return operation.promise(); }) .then(responses => { @@ -394,9 +397,9 @@ describe('longrunning', () => { }); const apiCall = createApiCall(func, client); - apiCall() + apiCall({}) .then(responses => { - const operation = responses[0]; + const operation = responses[0] as longrunning.Operation; return operation.promise(); }) .then(() => { @@ -416,9 +419,9 @@ describe('longrunning', () => { }; const client = mockOperationsClient({finalOperation: BAD_OP}); const apiCall = createApiCall(func, client); - apiCall() + apiCall({}) .then(responses => { - const operation = responses[0]; + const operation = responses[0] as longrunning.Operation; const promise = operation.promise(); return promise; }) @@ -433,8 +436,8 @@ describe('longrunning', () => { it('uses provided promise constructor', done => { const client = mockOperationsClient(); - const desc = new longrunning.LongrunningDescriptor( - client, mockDecoder, mockDecoder); + const desc = + new LongrunningDescriptor(client, mockDecoder, mockDecoder); const initialRetryDelayMillis = 1; const retryDelayMultiplier = 2; const maxRetryDelayMillis = 3; @@ -470,9 +473,9 @@ describe('longrunning', () => { }); const apiCall = createApiCall(func, client); - apiCall() + apiCall({}) .then(responses => { - const operation = responses[0]; + const operation = responses[0] as longrunning.Operation; const p = operation.promise(); operation.cancel().then(() => { // tslint:disable-next-line no-unused-expression @@ -502,9 +505,9 @@ describe('longrunning', () => { expectedCalls, }); const apiCall = createApiCall(func, client); - apiCall() + apiCall({}) .then(responses => { - const operation = responses[0]; + const operation = responses[0] as longrunning.Operation; operation.on('complete', (result, metadata, rawResponse) => { expect(result).to.deep.eq(RESPONSE_VAL); expect(metadata).to.deep.eq(METADATA_VAL); @@ -531,9 +534,9 @@ describe('longrunning', () => { finalOperation: ERROR_OP, }); const apiCall = createApiCall(func, client); - apiCall() + apiCall({}) .then(responses => { - const operation = responses[0]; + const operation = responses[0] as longrunning.Operation; operation.on('complete', () => { done(new Error('Should not get here.')); }); @@ -573,9 +576,9 @@ describe('longrunning', () => { finalOperation: updatedOp, }); const apiCall = createApiCall(func, client); - apiCall() + apiCall({}) .then(responses => { - const operation = responses[0]; + const operation = responses[0] as longrunning.Operation; operation.on('complete', () => { done(new Error('Should not get here.')); }); @@ -605,12 +608,12 @@ describe('longrunning', () => { finalOperation: PENDING_OP, }); const apiCall = createApiCall(func, client); - apiCall(null, { + // @ts-ignore incomplete options + apiCall({}, { longrunning: gax.createBackoffSettings(1, 1, 1, 0, 0, 0, 1), }) .then(responses => { - const operation = responses[0]; - console.log(operation); + const operation = responses[0] as longrunning.Operation; operation.on('complete', () => { done(new Error('Should not get here.')); }); diff --git a/test/pagedIteration.ts b/test/pagedIteration.ts index 7b49b8c9d..cf780f249 100644 --- a/test/pagedIteration.ts +++ b/test/pagedIteration.ts @@ -1,4 +1,4 @@ -/* Copyright 2016, Google Inc. +/* Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,7 +33,7 @@ import * as pumpify from 'pumpify'; import * as sinon from 'sinon'; import * as streamEvents from 'stream-events'; import * as through2 from 'through2'; -import {PageDescriptor} from '../src/pagedIteration'; +import {PageDescriptor} from '../src/paginationCalls/pageDescriptor'; import * as util from './utils'; @@ -66,7 +66,7 @@ describe('paged iteration', () => { for (let i = 0; i < pageSize * pagesToStream; ++i) { expected.push(i); } - apiCall({}, null) + apiCall({}, undefined) .then(results => { expect(results).to.be.an('array'); expect(results[0]).to.deep.equal(expected); @@ -81,7 +81,7 @@ describe('paged iteration', () => { for (let i = 0; i < pageSize * pagesToStream; ++i) { expected.push(i); } - apiCall({}, null, (err, results) => { + apiCall({}, undefined, (err, results) => { // tslint:disable-next-line no-unused-expression expect(err).to.be.null; expect(results).to.deep.equal(expected); @@ -97,8 +97,10 @@ describe('paged iteration', () => { .then(response => { expect(response).to.be.an('array'); expect(response[0]).to.be.an('array'); + // @ts-ignore response type expect(response[0].length).to.eq(pageSize); for (let i = 0; i < pageSize; ++i) { + // @ts-ignore response type expect(response[0][i]).to.eq(expected); expected++; } @@ -106,6 +108,7 @@ describe('paged iteration', () => { expect(response[1]).to.have.property('pageToken'); expect(response[2]).to.be.an('object'); expect(response[2]).to.have.property('nums'); + // @ts-ignore response type return apiCall(response[1], {autoPaginate: false}); }) .then(response => { @@ -155,9 +158,10 @@ describe('paged iteration', () => { } } const apiCall = util.createApiCall(failingFunc, createOptions); - apiCall({}, null) + apiCall({}, undefined) .then(resources => { expect(resources).to.be.an('array'); + // @ts-ignore response type expect(resources[0].length).to.eq(pageSize * pagesToStream); done(); }) @@ -170,9 +174,12 @@ describe('paged iteration', () => { return apiCall({}, {maxResults: pageSize * 2 + 2}).then(response => { expect(response).to.be.an('array'); expect(response[0]).to.be.an('array'); + // @ts-ignore response type expect(response[0].length).to.eq(pageSize * 2 + 2); let expected = 0; + // @ts-ignore response type for (let i = 0; i < response[0].length; ++i) { + // @ts-ignore response type expect(response[0][i]).to.eq(expected); expected++; } @@ -205,12 +212,14 @@ describe('paged iteration', () => { } it('returns a stream', done => { + // @ts-ignore incomplete options streamChecker(descriptor.createStream(apiCall, {}, null), () => { expect(spy.callCount).to.eq(pagesToStream + 1); }, done, 0); }); it('stops in the middle', done => { + // @ts-ignore incomplete options const stream = descriptor.createStream(apiCall, {}, null); stream.on('data', data => { if (Number(data) === pageSize + 1) { @@ -227,6 +236,7 @@ describe('paged iteration', () => { // pageSize which will be used so that the stream will start from the // specified token. const options = {pageToken: pageSize, autoPaginate: false}; + // @ts-ignore incomplete options streamChecker(descriptor.createStream(apiCall, {}, options), () => { expect(spy.callCount).to.eq(pagesToStream); }, done, pageSize); @@ -235,6 +245,7 @@ describe('paged iteration', () => { it('caps the elements by maxResults', done => { const onData = sinon.spy(); const stream = + // @ts-ignore incomplete options descriptor.createStream(apiCall, {}, {maxResults: pageSize * 2 + 2}); stream.on('data', onData); streamChecker(stream, () => { @@ -244,6 +255,7 @@ describe('paged iteration', () => { }); it('does not call API eagerly', done => { + // @ts-ignore incomplete options const stream = descriptor.createStream(apiCall, {}, null); setTimeout(() => { expect(spy.callCount).to.eq(0); @@ -254,6 +266,7 @@ describe('paged iteration', () => { }); it('does not start calls when it is already started', done => { + // @ts-ignore incomplete options const stream = descriptor.createStream(apiCall, {}, null); stream.on('end', () => { expect(spy.callCount).to.eq(pagesToStream + 1); @@ -271,6 +284,7 @@ describe('paged iteration', () => { // tslint:disable-next-line no-any const output = streamEvents((pumpify as any).obj()) as pumpify; output.once('reading', () => { + // @ts-ignore incomplete options stream = descriptor.createStream(apiCall, {}, null); output.setPipeline(stream, through2.obj()); }); diff --git a/test/path_template.ts b/test/path_template.ts index e9089714a..9cbfe8115 100644 --- a/test/path_template.ts +++ b/test/path_template.ts @@ -1,6 +1,5 @@ /* - * - * Copyright 2016, Google Inc. + * Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,7 +27,6 @@ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * */ import {expect} from 'chai'; diff --git a/test/routingHeader.ts b/test/routingHeader.ts index c4c5adf26..a5a6d35ff 100644 --- a/test/routingHeader.ts +++ b/test/routingHeader.ts @@ -1,4 +1,5 @@ -/* Copyright 2017, Google Inc. +/* + * Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/test/streaming.ts b/test/streaming.ts index 6a02df02a..ba3af9b56 100644 --- a/test/streaming.ts +++ b/test/streaming.ts @@ -1,4 +1,5 @@ -/* Copyright 2016, Google Inc. +/* + * Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -32,16 +33,17 @@ import {expect} from 'chai'; import * as sinon from 'sinon'; import * as through2 from 'through2'; -import * as apiCallable from '../src/apiCallable'; +import {GaxCallStream} from '../src/apitypes'; +import {createApiCall} from '../src/createApiCall'; import * as gax from '../src/gax'; -import * as streaming from '../src/streaming'; +import {StreamDescriptor} from '../src/streamingCalls/streamDescriptor'; +import * as streaming from '../src/streamingCalls/streaming'; -function createApiCall(func, type) { - // can't use "createApiCall" in util.js because argument list is different - // in streaming API call. +function createApiCallStreaming(func, type) { const settings = new gax.CallSettings(); - return apiCallable.createApiCall( - Promise.resolve(func), settings, new streaming.StreamDescriptor(type)); + return createApiCall( + Promise.resolve(func), settings, new StreamDescriptor(type)) as + GaxCallStream; } describe('streaming', () => { @@ -58,8 +60,9 @@ describe('streaming', () => { return s; }); - const apiCall = createApiCall(spy, streaming.StreamType.SERVER_STREAMING); - const s = apiCall(null, null); + const apiCall = + createApiCallStreaming(spy, streaming.StreamType.SERVER_STREAMING); + const s = apiCall({}, undefined); const callback = sinon.spy(data => { if (callback.callCount === 1) { expect(data).to.deep.equal({resources: [1, 2]}); @@ -93,8 +96,9 @@ describe('streaming', () => { return s; } - const apiCall = createApiCall(func, streaming.StreamType.CLIENT_STREAMING); - const s = apiCall(null, null, (err, response) => { + const apiCall = + createApiCallStreaming(func, streaming.StreamType.CLIENT_STREAMING); + const s = apiCall({}, undefined, (err, response) => { // tslint:disable-next-line no-unused-expression expect(err).to.be.null; expect(response).to.deep.eq(['foo', 'bar']); @@ -119,8 +123,9 @@ describe('streaming', () => { return s; } - const apiCall = createApiCall(func, streaming.StreamType.BIDI_STREAMING); - const s = apiCall(null, null); + const apiCall = + createApiCallStreaming(func, streaming.StreamType.BIDI_STREAMING); + const s = apiCall({}, undefined); const arg = {foo: 'bar'}; const callback = sinon.spy(data => { expect(data).to.eq(arg); @@ -158,11 +163,12 @@ describe('streaming', () => { }); return s; } - const apiCall = createApiCall(func, streaming.StreamType.BIDI_STREAMING); - const s = apiCall(null, null); - let receivedMetadata; - let receivedStatus; - let receivedResponse; + const apiCall = + createApiCallStreaming(func, streaming.StreamType.BIDI_STREAMING); + const s = apiCall({}, undefined); + let receivedMetadata: {}; + let receivedStatus: {}; + let receivedResponse: {}; s.on('metadata', data => { receivedMetadata = data; }); @@ -211,8 +217,9 @@ describe('streaming', () => { }); return s; } - const apiCall = createApiCall(func, streaming.StreamType.SERVER_STREAMING); - const s = apiCall(null, null); + const apiCall = + createApiCallStreaming(func, streaming.StreamType.SERVER_STREAMING); + const s = apiCall({}, undefined); let counter = 0; const expectedCount = 5; s.on('data', data => { diff --git a/test/utils.ts b/test/utils.ts index 62343a3fc..1af1b4b20 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,4 +1,5 @@ -/* Copyright 2016, Google Inc. +/* + * Copyright 2019 Google LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,9 +29,10 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import * as apiCallable from '../src/apiCallable'; +import {GaxCallPromise} from '../src/apitypes'; +import {createApiCall as realCreateApiCall} from '../src/createApiCall'; import * as gax from '../src/gax'; -import {GoogleError} from '../src/GoogleError'; +import {GoogleError} from '../src/googleError'; const FAKE_STATUS_CODE_1 = (exports.FAKE_STATUS_CODE_1 = 1); @@ -44,29 +46,29 @@ export function createApiCall(func, opts?) { opts = opts || {}; const settings = new gax.CallSettings(opts.settings || {}); const descriptor = opts.descriptor; - return apiCallable.createApiCall( - Promise.resolve((argument, metadata, options, callback) => { - if (opts.returnCancelFunc) { - return { - cancel: func(argument, metadata, options, callback), - completed: true, - call: () => { - throw new Error('should not be run'); - } - }; - } - func(argument, metadata, options, callback); - return { - cancel: opts.cancel || (() => { - callback(new Error('canceled')); - }), - completed: true, - call: () => { - throw new Error('should not be run'); - } - }; - }), - settings, descriptor); + return realCreateApiCall( + Promise.resolve((argument, metadata, options, callback) => { + if (opts.returnCancelFunc) { + return { + cancel: func(argument, metadata, options, callback), + completed: true, + call: () => { + throw new Error('should not be run'); + } + }; + } + func(argument, metadata, options, callback); + return { + cancel: opts.cancel || (() => { + callback(new Error('canceled')); + }), + completed: true, + call: () => { + throw new Error('should not be run'); + } + }; + }), + settings, descriptor) as GaxCallPromise; } export function createRetryOptions( diff --git a/test/warning.ts b/test/warning.ts index b236a8a1e..52dc34a84 100644 --- a/test/warning.ts +++ b/test/warning.ts @@ -1,17 +1,32 @@ -/** - * Copyright 2019 Google LLC. All Rights Reserved. +/* + * Copyright 2019 Google LLC + * All rights reserved. * - * 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 + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: * - * http://www.apache.org/licenses/LICENSE-2.0 + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. * - * 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 SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import {assert} from 'chai'; diff --git a/tsconfig.json b/tsconfig.json index ab692e1ec..2f74af95c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ }, "include": [ "src/*.ts", + "src/*/*.ts", "test/*.ts", "system-test/*.ts" ]