mirror of https://github.com/grpc/grpc-web.git
Add abort API support for promise calls (#1478)
For #1478 - Also removed CallOptions in favor of PromiseCallOptions (to align with internal codebase) - Also updated chrome to 112.0.5615.165 for supporting abort with reason.
This commit is contained in:
parent
b5ff5d303d
commit
7ea072a94d
|
@ -32,6 +32,20 @@ const MethodDescriptor = goog.require('grpc.web.MethodDescriptor');
|
||||||
const RpcError = goog.require('grpc.web.RpcError');
|
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
|
* This interface represents a grpc-web client
|
||||||
* @interface
|
* @interface
|
||||||
|
@ -62,10 +76,11 @@ const AbstractClientBase = class {
|
||||||
* @param {!Object<string, string>} metadata User defined call metadata
|
* @param {!Object<string, string>} metadata User defined call metadata
|
||||||
* @param {!MethodDescriptor<REQUEST, RESPONSE>}
|
* @param {!MethodDescriptor<REQUEST, RESPONSE>}
|
||||||
* methodDescriptor Information of this RPC method
|
* methodDescriptor Information of this RPC method
|
||||||
|
* @param options Options for the call
|
||||||
* @return {!IThenable<RESPONSE>}
|
* @return {!IThenable<RESPONSE>}
|
||||||
* A promise that resolves to the response message
|
* A promise that resolves to the response message
|
||||||
*/
|
*/
|
||||||
thenableCall(method, requestMessage, metadata, methodDescriptor) {}
|
thenableCall(method, requestMessage, metadata, methodDescriptor, options) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @abstract
|
* @abstract
|
||||||
|
@ -78,21 +93,19 @@ const AbstractClientBase = class {
|
||||||
* @return {!ClientReadableStream<RESPONSE>} The Client Readable Stream
|
* @return {!ClientReadableStream<RESPONSE>} The Client Readable Stream
|
||||||
*/
|
*/
|
||||||
serverStreaming(method, requestMessage, metadata, methodDescriptor) {}
|
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};
|
||||||
|
|
|
@ -28,7 +28,6 @@ goog.module('grpc.web.GrpcWebClientBase');
|
||||||
goog.module.declareLegacyNamespace();
|
goog.module.declareLegacyNamespace();
|
||||||
|
|
||||||
|
|
||||||
const AbstractClientBase = goog.require('grpc.web.AbstractClientBase');
|
|
||||||
const ClientOptions = goog.requireType('grpc.web.ClientOptions');
|
const ClientOptions = goog.requireType('grpc.web.ClientOptions');
|
||||||
const ClientReadableStream = goog.require('grpc.web.ClientReadableStream');
|
const ClientReadableStream = goog.require('grpc.web.ClientReadableStream');
|
||||||
const ClientUnaryCallImpl = goog.require('grpc.web.ClientUnaryCallImpl');
|
const ClientUnaryCallImpl = goog.require('grpc.web.ClientUnaryCallImpl');
|
||||||
|
@ -40,6 +39,7 @@ const RpcError = goog.require('grpc.web.RpcError');
|
||||||
const StatusCode = goog.require('grpc.web.StatusCode');
|
const StatusCode = goog.require('grpc.web.StatusCode');
|
||||||
const XhrIo = goog.require('goog.net.XhrIo');
|
const XhrIo = goog.require('goog.net.XhrIo');
|
||||||
const googCrypt = goog.require('goog.crypt.base64');
|
const googCrypt = goog.require('goog.crypt.base64');
|
||||||
|
const {AbstractClientBase, PromiseCallOptions, getHostname} = goog.require('grpc.web.AbstractClientBase');
|
||||||
const {Status} = goog.require('grpc.web.Status');
|
const {Status} = goog.require('grpc.web.Status');
|
||||||
const {StreamInterceptor, UnaryInterceptor} = goog.require('grpc.web.Interceptor');
|
const {StreamInterceptor, UnaryInterceptor} = goog.require('grpc.web.Interceptor');
|
||||||
const {toObject} = goog.require('goog.collections.maps');
|
const {toObject} = goog.require('goog.collections.maps');
|
||||||
|
@ -101,7 +101,7 @@ class GrpcWebClientBase {
|
||||||
* @export
|
* @export
|
||||||
*/
|
*/
|
||||||
rpcCall(method, requestMessage, metadata, methodDescriptor, callback) {
|
rpcCall(method, requestMessage, metadata, methodDescriptor, callback) {
|
||||||
const hostname = AbstractClientBase.getHostname(method, methodDescriptor);
|
const hostname = getHostname(method, methodDescriptor);
|
||||||
const invoker = GrpcWebClientBase.runInterceptors_(
|
const invoker = GrpcWebClientBase.runInterceptors_(
|
||||||
(request) => this.startStream_(request, hostname),
|
(request) => this.startStream_(request, hostname),
|
||||||
this.streamInterceptors_);
|
this.streamInterceptors_);
|
||||||
|
@ -112,18 +112,35 @@ class GrpcWebClientBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override
|
* @param {string} method The method to invoke
|
||||||
* @export
|
* @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) {
|
thenableCall(
|
||||||
const hostname = AbstractClientBase.getHostname(method, methodDescriptor);
|
method, requestMessage, metadata, methodDescriptor, options = {}) {
|
||||||
|
const hostname = getHostname(method, methodDescriptor);
|
||||||
|
const signal = options && options.signal;
|
||||||
const initialInvoker = (request) => new Promise((resolve, reject) => {
|
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);
|
const stream = this.startStream_(request, hostname);
|
||||||
let unaryMetadata;
|
let unaryMetadata;
|
||||||
let unaryStatus;
|
let unaryStatus;
|
||||||
let unaryMsg;
|
let unaryMsg;
|
||||||
GrpcWebClientBase.setCallback_(
|
GrpcWebClientBase.setCallback_(
|
||||||
stream, (error, response, status, metadata, unaryResponseReceived) => {
|
stream,
|
||||||
|
(error, response, status, metadata, unaryResponseReceived) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
} else if (unaryResponseReceived) {
|
} else if (unaryResponseReceived) {
|
||||||
|
@ -136,7 +153,19 @@ class GrpcWebClientBase {
|
||||||
resolve(request.getMethodDescriptor().createUnaryResponse(
|
resolve(request.getMethodDescriptor().createUnaryResponse(
|
||||||
unaryMsg, unaryMetadata, unaryStatus));
|
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_(
|
const invoker = GrpcWebClientBase.runInterceptors_(
|
||||||
initialInvoker, this.unaryInterceptors_);
|
initialInvoker, this.unaryInterceptors_);
|
||||||
|
@ -152,12 +181,13 @@ class GrpcWebClientBase {
|
||||||
* @param {!Object<string, string>} metadata User defined call metadata
|
* @param {!Object<string, string>} metadata User defined call metadata
|
||||||
* @param {!MethodDescriptor<REQUEST, RESPONSE>} methodDescriptor Information
|
* @param {!MethodDescriptor<REQUEST, RESPONSE>} methodDescriptor Information
|
||||||
* of this RPC method
|
* of this RPC method
|
||||||
|
* @param {?PromiseCallOptions=} options Options for the call
|
||||||
* @return {!Promise<RESPONSE>}
|
* @return {!Promise<RESPONSE>}
|
||||||
* @template REQUEST, RESPONSE
|
* @template REQUEST, RESPONSE
|
||||||
*/
|
*/
|
||||||
unaryCall(method, requestMessage, metadata, methodDescriptor) {
|
unaryCall(method, requestMessage, metadata, methodDescriptor, options = {}) {
|
||||||
return /** @type {!Promise<RESPONSE>}*/ (
|
return /** @type {!Promise<RESPONSE>}*/ (this.thenableCall(
|
||||||
this.thenableCall(method, requestMessage, metadata, methodDescriptor));
|
method, requestMessage, metadata, methodDescriptor, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -165,7 +195,7 @@ class GrpcWebClientBase {
|
||||||
* @export
|
* @export
|
||||||
*/
|
*/
|
||||||
serverStreaming(method, requestMessage, metadata, methodDescriptor) {
|
serverStreaming(method, requestMessage, metadata, methodDescriptor) {
|
||||||
const hostname = AbstractClientBase.getHostname(method, methodDescriptor);
|
const hostname = getHostname(method, methodDescriptor);
|
||||||
const invoker = GrpcWebClientBase.runInterceptors_(
|
const invoker = GrpcWebClientBase.runInterceptors_(
|
||||||
(request) => this.startStream_(request, hostname),
|
(request) => this.startStream_(request, hostname),
|
||||||
this.streamInterceptors_);
|
this.streamInterceptors_);
|
||||||
|
@ -279,7 +309,9 @@ class GrpcWebClientBase {
|
||||||
message: 'Incomplete response',
|
message: 'Incomplete response',
|
||||||
});
|
});
|
||||||
} else if (useUnaryResponse) {
|
} else if (useUnaryResponse) {
|
||||||
callback(null, responseReceived, null, null, /* unaryResponseReceived= */ true);
|
callback(
|
||||||
|
null, responseReceived, null, null,
|
||||||
|
/* unaryResponseReceived= */ true);
|
||||||
} else {
|
} else {
|
||||||
callback(null, responseReceived);
|
callback(null, responseReceived);
|
||||||
}
|
}
|
||||||
|
@ -368,12 +400,9 @@ class GrpcWebClientBase {
|
||||||
* (!Promise<RESPONSE>|!ClientReadableStream<RESPONSE>)}
|
* (!Promise<RESPONSE>|!ClientReadableStream<RESPONSE>)}
|
||||||
*/
|
*/
|
||||||
static runInterceptors_(invoker, interceptors) {
|
static runInterceptors_(invoker, interceptors) {
|
||||||
let curInvoker = invoker;
|
return interceptors.reduce((accumulatedInvoker, interceptor) => {
|
||||||
interceptors.forEach((interceptor) => {
|
return (request) => interceptor.intercept(request, accumulatedInvoker);
|
||||||
const lastInvoker = curInvoker;
|
}, invoker);
|
||||||
curInvoker = (request) => interceptor.intercept(request, lastInvoker);
|
|
||||||
});
|
|
||||||
return curInvoker;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ goog.module('grpc.web.GrpcWebClientBaseTest');
|
||||||
goog.setTestOnly('grpc.web.GrpcWebClientBaseTest');
|
goog.setTestOnly('grpc.web.GrpcWebClientBaseTest');
|
||||||
|
|
||||||
const ClientReadableStream = goog.require('grpc.web.ClientReadableStream');
|
const ClientReadableStream = goog.require('grpc.web.ClientReadableStream');
|
||||||
|
const ErrorCode = goog.require('goog.net.ErrorCode');
|
||||||
const GrpcWebClientBase = goog.require('grpc.web.GrpcWebClientBase');
|
const GrpcWebClientBase = goog.require('grpc.web.GrpcWebClientBase');
|
||||||
const MethodDescriptor = goog.require('grpc.web.MethodDescriptor');
|
const MethodDescriptor = goog.require('grpc.web.MethodDescriptor');
|
||||||
const ReadyState = goog.require('goog.net.XmlHttp.ReadyState');
|
const ReadyState = goog.require('goog.net.XmlHttp.ReadyState');
|
||||||
|
@ -146,6 +147,58 @@ testSuite({
|
||||||
assertElementsEquals(DEFAULT_UNARY_HEADER_VALUES, Object.values(headers));
|
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() {
|
async testDeadline() {
|
||||||
const xhr = new XhrIo();
|
const xhr = new XhrIo();
|
||||||
const client = new GrpcWebClientBase(/* options= */ {}, xhr);
|
const client = new GrpcWebClientBase(/* options= */ {}, xhr);
|
||||||
|
|
|
@ -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.
|
# Matching the node version used in the node:20.0.0-bullseye image.
|
||||||
ARG NODE_VERSION=20.0.0
|
ARG NODE_VERSION=20.0.0
|
||||||
|
|
|
@ -7,7 +7,8 @@ declare module "grpc-web" {
|
||||||
method: string,
|
method: string,
|
||||||
request: REQ,
|
request: REQ,
|
||||||
metadata: Metadata,
|
metadata: Metadata,
|
||||||
methodDescriptor: MethodDescriptor<REQ, RESP>
|
methodDescriptor: MethodDescriptor<REQ, RESP>,
|
||||||
|
options?: PromiseCallOptions
|
||||||
): Promise<RESP>;
|
): Promise<RESP>;
|
||||||
|
|
||||||
rpcCall<REQ, RESP> (
|
rpcCall<REQ, RESP> (
|
||||||
|
@ -64,8 +65,10 @@ declare module "grpc-web" {
|
||||||
Promise<UnaryResponse<REQ, RESP>>): Promise<UnaryResponse<REQ, RESP>>;
|
Promise<UnaryResponse<REQ, RESP>>): Promise<UnaryResponse<REQ, RESP>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CallOptions {
|
/** Options for gRPC-Web calls returning a Promise. */
|
||||||
constructor(options: { [index: string]: any; });
|
export interface PromiseCallOptions {
|
||||||
|
/** An AbortSignal to abort the call. */
|
||||||
|
readonly signal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MethodDescriptor<REQ, RESP> {
|
export class MethodDescriptor<REQ, RESP> {
|
||||||
|
@ -82,7 +85,6 @@ declare module "grpc-web" {
|
||||||
getRequestMessage(): REQ;
|
getRequestMessage(): REQ;
|
||||||
getMethodDescriptor(): MethodDescriptor<REQ, RESP>;
|
getMethodDescriptor(): MethodDescriptor<REQ, RESP>;
|
||||||
getMetadata(): Metadata;
|
getMetadata(): Metadata;
|
||||||
getCallOptions(): CallOptions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnaryResponse<REQ, RESP> {
|
export class UnaryResponse<REQ, RESP> {
|
||||||
|
|
|
@ -44,8 +44,8 @@ trap cleanup EXIT
|
||||||
|
|
||||||
echo "Using Headless Chrome."
|
echo "Using Headless Chrome."
|
||||||
# Updates Selenium Webdriver.
|
# Updates Selenium Webdriver.
|
||||||
echo "$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=93.0.4577.63 --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)
|
# Run the tests using Protractor! (Protractor should run selenium automatically)
|
||||||
$PROTRACTOR_BIN_PATH/protractor protractor.conf.js
|
$PROTRACTOR_BIN_PATH/protractor protractor.conf.js
|
||||||
|
|
Loading…
Reference in New Issue