Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 28 additions & 15 deletions javascript/net/grpc/web/abstractclientbase.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ const MethodDescriptor = goog.require('grpc.web.MethodDescriptor');
const RpcError = goog.require('grpc.web.RpcError');


/**
* @constructor
* @struct
* @final
*/
const PromiseCallOptions = function() {};

/**
* An AbortSignal to abort the call.
* @type {AbortSignal|undefined}
*/
PromiseCallOptions.prototype.signal;


/**
* This interface represents a grpc-web client
* @interface
Expand Down Expand Up @@ -62,10 +76,11 @@ const AbstractClientBase = class {
* @param {!Object<string, string>} metadata User defined call metadata
* @param {!MethodDescriptor<REQUEST, RESPONSE>}
* methodDescriptor Information of this RPC method
* @param options Options for the call
* @return {!IThenable<RESPONSE>}
* A promise that resolves to the response message
*/
thenableCall(method, requestMessage, metadata, methodDescriptor) {}
thenableCall(method, requestMessage, metadata, methodDescriptor, options) {}

/**
* @abstract
Expand All @@ -78,21 +93,19 @@ const AbstractClientBase = class {
* @return {!ClientReadableStream<RESPONSE>} The Client Readable Stream
*/
serverStreaming(method, requestMessage, metadata, methodDescriptor) {}

/**
* Get the hostname of the current request.
* @static
* @template REQUEST, RESPONSE
* @param {string} method
* @param {!MethodDescriptor<REQUEST,RESPONSE>} methodDescriptor
* @return {string}
*/
static getHostname(method, methodDescriptor) {
// method = hostname + methodDescriptor.name(relative path of this method)
return method.substr(0, method.length - methodDescriptor.name.length);
}
};

/**
* Get the hostname of the current request.
* @template REQUEST, RESPONSE
* @param {string} method
* @param {!MethodDescriptor<REQUEST,RESPONSE>} methodDescriptor
* @return {string}
*/
function getHostname(method, methodDescriptor) {
// method = hostname + methodDescriptor.name(relative path of this method)
return method.substr(0, method.length - methodDescriptor.name.length);
}


exports = AbstractClientBase;
exports = {AbstractClientBase, PromiseCallOptions, getHostname};
67 changes: 48 additions & 19 deletions javascript/net/grpc/web/grpcwebclientbase.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ goog.module('grpc.web.GrpcWebClientBase');
goog.module.declareLegacyNamespace();


const AbstractClientBase = goog.require('grpc.web.AbstractClientBase');
const ClientOptions = goog.requireType('grpc.web.ClientOptions');
const ClientReadableStream = goog.require('grpc.web.ClientReadableStream');
const ClientUnaryCallImpl = goog.require('grpc.web.ClientUnaryCallImpl');
Expand All @@ -40,6 +39,7 @@ const RpcError = goog.require('grpc.web.RpcError');
const StatusCode = goog.require('grpc.web.StatusCode');
const XhrIo = goog.require('goog.net.XhrIo');
const googCrypt = goog.require('goog.crypt.base64');
const {AbstractClientBase, PromiseCallOptions, getHostname} = goog.require('grpc.web.AbstractClientBase');
const {Status} = goog.require('grpc.web.Status');
const {StreamInterceptor, UnaryInterceptor} = goog.require('grpc.web.Interceptor');
const {toObject} = goog.require('goog.collections.maps');
Expand Down Expand Up @@ -101,7 +101,7 @@ class GrpcWebClientBase {
* @export
*/
rpcCall(method, requestMessage, metadata, methodDescriptor, callback) {
const hostname = AbstractClientBase.getHostname(method, methodDescriptor);
const hostname = getHostname(method, methodDescriptor);
const invoker = GrpcWebClientBase.runInterceptors_(
(request) => this.startStream_(request, hostname),
this.streamInterceptors_);
Expand All @@ -112,18 +112,35 @@ class GrpcWebClientBase {
}

/**
* @override
* @export
* @param {string} method The method to invoke
* @param {REQUEST} requestMessage The request proto
* @param {!Object<string, string>} metadata User defined call metadata
* @param {!MethodDescriptor<REQUEST, RESPONSE>} methodDescriptor
* @param {?PromiseCallOptions=} options Options for the call
* @return {!Promise<RESPONSE>}
* @template REQUEST, RESPONSE
*/
thenableCall(method, requestMessage, metadata, methodDescriptor) {
const hostname = AbstractClientBase.getHostname(method, methodDescriptor);
thenableCall(
method, requestMessage, metadata, methodDescriptor, options = {}) {
const hostname = getHostname(method, methodDescriptor);
const signal = options && options.signal;
const initialInvoker = (request) => new Promise((resolve, reject) => {
// If the signal is already aborted, immediately reject the promise
// and don't issue the call.
if (signal && signal.aborted) {
const error = new RpcError(StatusCode.CANCELLED, 'Aborted');
error.cause = signal.reason;
reject(error);
return;
}

const stream = this.startStream_(request, hostname);
let unaryMetadata;
let unaryStatus;
let unaryMsg;
GrpcWebClientBase.setCallback_(
stream, (error, response, status, metadata, unaryResponseReceived) => {
stream,
(error, response, status, metadata, unaryResponseReceived) => {
if (error) {
reject(error);
} else if (unaryResponseReceived) {
Expand All @@ -136,7 +153,19 @@ class GrpcWebClientBase {
resolve(request.getMethodDescriptor().createUnaryResponse(
unaryMsg, unaryMetadata, unaryStatus));
}
}, true);
},
true);

// Wire up cancellation from the abort signal, if any.
if (signal) {
signal.addEventListener('abort', () => {
stream.cancel();

const error = new RpcError(StatusCode.CANCELLED, 'Aborted');
error.cause = /** @type {!AbortSignal} */ (signal).reason;
reject(error);
});
}
});
const invoker = GrpcWebClientBase.runInterceptors_(
initialInvoker, this.unaryInterceptors_);
Expand All @@ -152,20 +181,21 @@ class GrpcWebClientBase {
* @param {!Object<string, string>} metadata User defined call metadata
* @param {!MethodDescriptor<REQUEST, RESPONSE>} methodDescriptor Information
* of this RPC method
* @param {?PromiseCallOptions=} options Options for the call
* @return {!Promise<RESPONSE>}
* @template REQUEST, RESPONSE
*/
unaryCall(method, requestMessage, metadata, methodDescriptor) {
return /** @type {!Promise<RESPONSE>}*/ (
this.thenableCall(method, requestMessage, metadata, methodDescriptor));
unaryCall(method, requestMessage, metadata, methodDescriptor, options = {}) {
return /** @type {!Promise<RESPONSE>}*/ (this.thenableCall(
method, requestMessage, metadata, methodDescriptor, options));
}

/**
* @override
* @export
*/
serverStreaming(method, requestMessage, metadata, methodDescriptor) {
const hostname = AbstractClientBase.getHostname(method, methodDescriptor);
const hostname = getHostname(method, methodDescriptor);
const invoker = GrpcWebClientBase.runInterceptors_(
(request) => this.startStream_(request, hostname),
this.streamInterceptors_);
Expand Down Expand Up @@ -279,7 +309,9 @@ class GrpcWebClientBase {
message: 'Incomplete response',
});
} else if (useUnaryResponse) {
callback(null, responseReceived, null, null, /* unaryResponseReceived= */ true);
callback(
null, responseReceived, null, null,
/* unaryResponseReceived= */ true);
} else {
callback(null, responseReceived);
}
Expand Down Expand Up @@ -368,12 +400,9 @@ class GrpcWebClientBase {
* (!Promise<RESPONSE>|!ClientReadableStream<RESPONSE>)}
*/
static runInterceptors_(invoker, interceptors) {
let curInvoker = invoker;
interceptors.forEach((interceptor) => {
const lastInvoker = curInvoker;
curInvoker = (request) => interceptor.intercept(request, lastInvoker);
});
return curInvoker;
return interceptors.reduce((accumulatedInvoker, interceptor) => {
return (request) => interceptor.intercept(request, accumulatedInvoker);
}, invoker);
}
}

Expand Down
53 changes: 53 additions & 0 deletions javascript/net/grpc/web/grpcwebclientbase_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ goog.module('grpc.web.GrpcWebClientBaseTest');
goog.setTestOnly('grpc.web.GrpcWebClientBaseTest');

const ClientReadableStream = goog.require('grpc.web.ClientReadableStream');
const ErrorCode = goog.require('goog.net.ErrorCode');
const GrpcWebClientBase = goog.require('grpc.web.GrpcWebClientBase');
const MethodDescriptor = goog.require('grpc.web.MethodDescriptor');
const ReadyState = goog.require('goog.net.XmlHttp.ReadyState');
Expand Down Expand Up @@ -146,6 +147,58 @@ testSuite({
assertElementsEquals(DEFAULT_UNARY_HEADER_VALUES, Object.values(headers));
},


async testCancelledThenableCall() {
const xhr = new XhrIo();
const client = new GrpcWebClientBase(/* options= */ {}, xhr);
const methodDescriptor = createMethodDescriptor((bytes) => {
assertElementsEquals(DEFAULT_RPC_RESPONSE_DATA, [].slice.call(bytes));
return 0;
});

const abortController = new AbortController();
const signal = abortController.signal;
const responsePromise = client.thenableCall(
'url', new MockRequest(), /* metadata= */ {}, methodDescriptor,
{signal});
abortController.abort();

const error = await assertRejects(responsePromise);
assertTrue(error instanceof RpcError);
assertEquals(StatusCode.CANCELLED, /** @type {!RpcError} */ (error).code);
assertEquals('Aborted', /** @type {!RpcError} */ (error).message);
// Default abort reason if none provided.
const cause = /** @type {!RpcError} */ (error).cause;
assertTrue(cause instanceof Error);
assertEquals('AbortError', /** @type {!Error} */ (cause).name);
assertEquals(ErrorCode.ABORT, xhr.getLastErrorCode());
},

async testCancelledThenableCallWithReason() {
const xhr = new XhrIo();
const client = new GrpcWebClientBase(/* options= */ {}, xhr);
const methodDescriptor = createMethodDescriptor((bytes) => {
assertElementsEquals(DEFAULT_RPC_RESPONSE_DATA, [].slice.call(bytes));
return 0;
});

const abortController = new AbortController();
const signal = abortController.signal;
const responsePromise = client.thenableCall(
'url', new MockRequest(), /* metadata= */ {}, methodDescriptor,
{signal});
abortController.abort('cancelling');

const error = await assertRejects(responsePromise);
assertTrue(error instanceof RpcError);
assertEquals(StatusCode.CANCELLED, /** @type {!RpcError} */ (error).code);
assertEquals('Aborted', /** @type {!RpcError} */ (error).message);
// Abort reason forwarded as cause.
const cause = /** @type {!RpcError} */ (error).cause;
assertEquals('cancelling', cause);
assertEquals(ErrorCode.ABORT, xhr.getLastErrorCode());
},

async testDeadline() {
const xhr = new XhrIo();
const client = new GrpcWebClientBase(/* options= */ {}, xhr);
Expand Down
2 changes: 1 addition & 1 deletion packages/grpc-web/docker/jsunit-test/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM selenium/standalone-chrome:93.0.4577.63
FROM selenium/standalone-chrome:112.0.5615.165

# Matching the node version used in the node:20.0.0-bullseye image.
ARG NODE_VERSION=20.0.0
Expand Down
10 changes: 6 additions & 4 deletions packages/grpc-web/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ declare module "grpc-web" {
method: string,
request: REQ,
metadata: Metadata,
methodDescriptor: MethodDescriptor<REQ, RESP>
methodDescriptor: MethodDescriptor<REQ, RESP>,
options?: PromiseCallOptions
): Promise<RESP>;

rpcCall<REQ, RESP> (
Expand Down Expand Up @@ -64,8 +65,10 @@ declare module "grpc-web" {
Promise<UnaryResponse<REQ, RESP>>): Promise<UnaryResponse<REQ, RESP>>;
}

export class CallOptions {
constructor(options: { [index: string]: any; });
/** Options for gRPC-Web calls returning a Promise. */
export interface PromiseCallOptions {
/** An AbortSignal to abort the call. */
readonly signal?: AbortSignal;
}

export class MethodDescriptor<REQ, RESP> {
Expand All @@ -82,7 +85,6 @@ declare module "grpc-web" {
getRequestMessage(): REQ;
getMethodDescriptor(): MethodDescriptor<REQ, RESP>;
getMetadata(): Metadata;
getCallOptions(): CallOptions;
}

export class UnaryResponse<REQ, RESP> {
Expand Down
4 changes: 2 additions & 2 deletions packages/grpc-web/scripts/run_jsunit_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ trap cleanup EXIT

echo "Using Headless Chrome."
# Updates Selenium Webdriver.
echo "$PROTRACTOR_BIN_PATH/webdriver-manager update --versions.chrome=93.0.4577.63 --gecko=false"
$PROTRACTOR_BIN_PATH/webdriver-manager update --versions.chrome=93.0.4577.63 --gecko=false
echo "$PROTRACTOR_BIN_PATH/webdriver-manager update --versions.chrome=112.0.5615.165 --gecko=false"
$PROTRACTOR_BIN_PATH/webdriver-manager update --versions.chrome=112.0.5615.165 --gecko=false

# Run the tests using Protractor! (Protractor should run selenium automatically)
$PROTRACTOR_BIN_PATH/protractor protractor.conf.js