Skip to content

Commit

Permalink
feat: add AbortController for node-fetch (#569)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-fenster authored Aug 9, 2019
1 parent a1e6570 commit 92b7590
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 5 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"@grpc/grpc-js": "^0.5.2",
"@grpc/proto-loader": "^0.5.1",
"abort-controller": "^3.0.0",
"duplexify": "^3.6.0",
"google-auth-library": "^5.0.0",
"is-stream-ended": "^0.1.4",
Expand Down Expand Up @@ -93,7 +94,7 @@
"test": "nyc mocha build/test",
"lint": "gts check && eslint samples/*.js samples/**/*.js",
"clean": "gts clean",
"compile": "tsc -p . && cp src/*.json build/src && cp src/*.js build/src",
"compile": "tsc -p . && cp src/*.json build/src && cp src/*.js build/src && cp -r system-test/fixtures build/system-test/",
"compile-protos": "pbjs -t json ./protos/google/longrunning/operations.proto -p ./protos > protos/operations.json",
"fix": "gts fix && eslint --fix samples/*.js samples/**/*.js",
"prepare": "npm run compile && node ./build/tools/prepublish.js",
Expand Down
19 changes: 15 additions & 4 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import * as protobuf from 'protobufjs';
import * as gax from './gax';
import * as nodeFetch from 'node-fetch';
import * as routingHeader from './routingHeader';
import {AbortController as NodeAbortController} from 'abort-controller';
import {Status} from './status';
import {OutgoingHttpHeaders} from 'http';
import {
Expand Down Expand Up @@ -181,6 +182,11 @@ export class GrpcClient {
* @return {Promise} A promise which resolves to a gRPC-fallback service stub, which is a protobuf.js service stub instance modified to match the gRPC stub API
*/
async createStub(service: protobuf.Service, opts: ClientStubOptions) {
// an RPC function to be passed to protobufjs RPC API
function serviceClientImpl(method, requestData, callback) {
return [method, requestData, callback];
}

if (!this.authClient) {
if (this.auth && 'getClient' in this.auth) {
this.authClient = await this.auth.getClient();
Expand All @@ -192,9 +198,6 @@ export class GrpcClient {
throw new Error('No authentication was provided');
}
const authHeader = await this.authClient.getRequestHeaders();
function serviceClientImpl(method, requestData, callback) {
return [method, requestData, callback];
}
const serviceStub = service.create(serviceClientImpl, false, false);
const methods = this.getServiceMethods(service);

Expand All @@ -206,16 +209,22 @@ export class GrpcClient {
].apply(serviceStub, [req, callback]);

let cancelController, cancelSignal;
if (typeof AbortController !== 'undefined') {
if (isBrowser && typeof AbortController !== 'undefined') {
cancelController = new AbortController();
} else {
cancelController = new NodeAbortController();
}
if (cancelController) {
cancelSignal = cancelController.signal;
}
let cancelRequested = false;

const headers = Object.assign({}, authHeader);
headers['Content-Type'] = 'application/x-protobuf';
for (const key of Object.keys(options)) {
headers[key] = options[key][0];
}

const grpcFallbackProtocol = opts.protocol || 'https';
let servicePath = opts.servicePath;
if (!servicePath) {
Expand All @@ -226,6 +235,7 @@ export class GrpcClient {
return;
}
}

let servicePort;
const match = servicePath.match(/^(.*):(\d+)$/);
if (match) {
Expand All @@ -237,6 +247,7 @@ export class GrpcClient {
} else if (!servicePort) {
servicePort = 443;
}

const protoNamespaces: string[] = [];
let currNamespace = method.parent;
while (currNamespace.name !== '') {
Expand Down
220 changes: 220 additions & 0 deletions test/grpc-fallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/**
* 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 assert from 'assert';
import * as nodeFetch from 'node-fetch';
import * as abortController from 'abort-controller';
import * as protobuf from 'protobufjs';
import * as sinon from 'sinon';
import {echoProtoJson} from '../browser-test/echoProtoJson';
import {expect} from 'chai';
import {GrpcClient} from '../src/browser';

const authClient = {
getRequestHeaders() {
return {Authorization: 'Bearer SOME_TOKEN'};
},
};

const authStub = {
getClient() {
return Promise.resolve(authClient);
},
};

const opts = {
auth: authStub,
};

describe('loadProto', () => {
it('should create a root object', () => {
// @ts-ignore incomplete options
const gaxGrpc = new GrpcClient(opts);
const protos = gaxGrpc.loadProto(echoProtoJson);

assert(protos instanceof protobuf.Root);
assert(protos.lookupService('Echo') instanceof protobuf.Service);
assert(protos.lookupType('EchoRequest') instanceof protobuf.Type);
});

it('should be able to load no files', () => {
// @ts-ignore incomplete options
const gaxGrpc = new GrpcClient(opts);
const protos = gaxGrpc.loadProto({});
assert(protos instanceof protobuf.Root);

assert(protos.nested === undefined);
assert.strictEqual(protos.nested, undefined);
});
});

describe('createStub', () => {
let gaxGrpc, protos, echoService, stubOptions, stubExtraOptions;

beforeEach(() => {
// @ts-ignore incomplete options
gaxGrpc = new GrpcClient(opts);
protos = gaxGrpc.loadProto(echoProtoJson);
echoService = protos.lookupService('Echo');
stubOptions = {
servicePath: 'foo.example.com',
port: 443,
};
stubExtraOptions = {
servicePath: 'foo.example.com',
port: 443,
other_dummy_options: 'test',
};
});

it('should create a stub', async () => {
// tslint:disable-next-line no-any
const echoStub: any = await gaxGrpc.createStub(echoService, stubOptions);

assert(echoStub instanceof protobuf.rpc.Service);

// The stub should consist of service methods
expect(echoStub.echo).to.be.a('Function');
expect(echoStub.pagedExpand).to.be.a('Function');
expect(echoStub.wait).to.be.a('Function');

// There should be 6 methods for the echo service (and 4 other methods in the object)
assert.strictEqual(Object.keys(echoStub).length, 10);

// Each of the service methods should take 4 arguments (so that it works with createApiCall)
assert.strictEqual(echoStub.echo.length, 4);
});

it('should support optional parameters', async () => {
// tslint:disable-next-line no-any
const echoStub: any = await gaxGrpc.createStub(
echoService,
stubExtraOptions
);

assert(echoStub instanceof protobuf.rpc.Service);

// The stub should consist of methods
expect(echoStub.echo).to.be.a('Function');
expect(echoStub.collect).to.be.a('Function');
expect(echoStub.chat).to.be.a('Function');

// There should be 6 methods for the echo service (and 4 other members in the object)
assert.strictEqual(Object.keys(echoStub).length, 10);

// Each of the service methods should take 4 arguments (so that it works with createApiCall)
assert.strictEqual(echoStub.echo.length, 4);
});
});

describe('grpc-fallback', () => {
let gaxGrpc, protos, echoService, stubOptions;
const createdAbortControllers = [];
// @ts-ignore
const savedAbortController = abortController.AbortController;

before(() => {
stubOptions = {
servicePath: 'foo.example.com',
port: 443,
};

// @ts-ignore incomplete options
gaxGrpc = new GrpcClient(opts);
protos = gaxGrpc.loadProto(echoProtoJson);
echoService = protos.lookupService('Echo');
stubOptions = {
servicePath: 'foo.example.com',
port: 443,
};

//tslint:disable-next-line variable-name
const AbortController = function() {
// @ts-ignore
this.abort = function() {
// @ts-ignore
this.abortCalled = true;
};
// @ts-ignore
createdAbortControllers.push(this);
};

// @ts-ignore
abortController.AbortController = AbortController;
});

beforeEach(() => {
createdAbortControllers.splice(0);
});

afterEach(() => {
sinon.restore();
});

after(() => {
// @ts-ignore
abortController.AbortController = savedAbortController;
});

it('should make a request', done => {
const requestObject = {content: 'test-content'};
const responseType = protos.lookupType('EchoResponse');
const response = responseType.create(requestObject); // request === response for EchoService

sinon.stub(nodeFetch, 'Promise').returns(
Promise.resolve({
arrayBuffer: () => {
return Promise.resolve(responseType.encode(response).finish());
},
})
);

gaxGrpc.createStub(echoService, stubOptions).then(echoStub => {
echoStub.echo(requestObject, {}, {}, (err, result) => {
assert.strictEqual(requestObject.content, result.content);
done();
});
});
});

it('should be able to cancel an API call using AbortController', async () => {
sinon.stub(nodeFetch, 'Promise').returns(Promise.resolve({}));

const echoStub = await gaxGrpc.createStub(echoService, stubOptions);
const request = {content: 'content' + new Date().toString()};
const call = echoStub.echo(request, {}, {}, () => {});

call.cancel();

// @ts-ignore
assert.strictEqual(createdAbortControllers[0].abortCalled, true);
});
});

0 comments on commit 92b7590

Please sign in to comment.