feat(functions): support function timeout (#3534)

Added support for configurable function timeout which allows you to set the timeout for a cloud function call in milliseconds.
This commit is contained in:
Russell Wheatley
2020-04-27 20:58:10 +01:00
committed by GitHub
parent e30c80dde9
commit 50c0f12ef0
7 changed files with 85 additions and 13 deletions

View File

@@ -1,3 +0,0 @@
## Change logs
See [rnfirebase.io/releases](https://rnfirebase.io/releases).

View File

@@ -19,13 +19,17 @@ package io.invertase.firebase.functions;
import android.content.Context;
import com.facebook.react.bridge.ReadableMap;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.FirebaseApp;
import com.google.firebase.functions.FirebaseFunctions;
import com.google.firebase.functions.HttpsCallableReference;
import io.invertase.firebase.common.UniversalFirebaseModule;
import java.util.concurrent.TimeUnit;
@SuppressWarnings("WeakerAccess")
public class UniversalFirebaseFunctionsModule extends UniversalFirebaseModule {
public static final String DATA_KEY = "data";
@@ -42,17 +46,24 @@ public class UniversalFirebaseFunctionsModule extends UniversalFirebaseModule {
String region,
String origin,
String name,
Object data
Object data,
ReadableMap options
) {
return Tasks.call(getExecutor(), () -> {
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
FirebaseFunctions functionsInstance = FirebaseFunctions.getInstance(firebaseApp, region);
HttpsCallableReference httpReference = functionsInstance.getHttpsCallable(name);
if (options.hasKey("timeout")) {
httpReference.setTimeout((long) options.getInt("timeout"), TimeUnit.SECONDS);
}
if (origin != null) {
functionsInstance.useFunctionsEmulator(origin);
}
return Tasks.await(functionsInstance.getHttpsCallable(name).call(data)).getData();
return Tasks.await(httpReference.call(data)).getData();
});
}

View File

@@ -52,6 +52,7 @@ public class ReactNativeFirebaseFunctionsModule extends ReactNativeFirebaseModul
String origin,
String name,
ReadableMap wrapper,
ReadableMap options,
Promise promise
) {
Task<Object> callMethodTask = module.httpsCallable(
@@ -59,7 +60,8 @@ public class ReactNativeFirebaseFunctionsModule extends ReactNativeFirebaseModul
region,
origin,
name,
wrapper.toHashMap().get(DATA_KEY)
wrapper.toHashMap().get(DATA_KEY),
options
);
// resolve
@@ -78,7 +80,10 @@ public class ReactNativeFirebaseFunctionsModule extends ReactNativeFirebaseModul
details = functionsException.getDetails();
code = functionsException.getCode().name();
message = functionsException.getMessage();
if (functionsException.getCause() instanceof IOException) {
String timeout = FirebaseFunctionsException.Code.DEADLINE_EXCEEDED.name();
Boolean isTimeout = code.contains(timeout);
if (functionsException.getCause() instanceof IOException && !isTimeout) {
// return UNAVAILABLE for network io errors, to match iOS
code = FirebaseFunctionsException.Code.UNAVAILABLE.name();
message = FirebaseFunctionsException.Code.UNAVAILABLE.name();

View File

@@ -286,5 +286,21 @@ describe('functions()', () => {
return Promise.resolve();
});
it('HttpsCallableOptions.timeout will error when timeout is exceeded', async () => {
const fnName = 'invertaseReactNativeFirebaseFunctionsEmulator';
const region = 'europe-west2';
const functions = firebase.app().functions(region);
functions.useFunctionsEmulator('http://api.rnfirebase.io');
try {
await functions.httpsCallable(fnName, { timeout: 1000 })({ testTimeout: '3000' });
return Promise.reject(new Error('Did not throw an Error.'));
} catch (error) {
error.message.should.containEql('DEADLINE').containEql('EXCEEDED');
return Promise.resolve();
}
});
});
});

View File

@@ -41,6 +41,8 @@
(NSString *) name
wrapper:
(NSDictionary *) wrapper
options:
(NSDictionary *) options
resolver:
(RCTPromiseResolveBlock) resolve
rejecter:
@@ -54,6 +56,10 @@
FIRHTTPSCallable *callable = [functions HTTPSCallableWithName:name];
if (options[@"timeout"]) {
callable.timeoutInterval = [options[@"timeout"] doubleValue];
}
[callable callWithObject:[wrapper valueForKey:@"data"] completion:^(FIRHTTPSCallableResult *_Nullable result, NSError *_Nullable error) {
if (error) {
NSObject *details = [NSNull null];

View File

@@ -144,6 +144,30 @@ export namespace FirebaseFunctionsTypes {
(data?: any): Promise<HttpsCallableResult>;
}
/**
* An HttpsCallableOptions object that can be passed as the second argument to `firebase.functions().httpsCallable(name, HttpsCallableOptions)`.
**/
export interface HttpsCallableOptions {
/**
* The timeout property allows you to control how long the application will wait for the cloud function to respond in milliseconds.
*
* #### Example
*
*```js
* // The below will wait 7 seconds for a response from the cloud function before an error is thrown
* try {
* const instance = firebase.functions().httpsCallable('order', { timeout: 7000 });
* const response = await instance({
* id: '12345',
* });
* } catch (e) {
* console.log(e);
* }
* ```
*/
timeout?: number;
}
/**
* An HttpsError wraps a single error from a function call.
*
@@ -312,7 +336,7 @@ export namespace FirebaseFunctionsTypes {
* @param name The name of the https callable function.
* @return The `HttpsCallable` instance.
*/
httpsCallable(name: string): HttpsCallable;
httpsCallable(name: string, options?: HttpsCallableOptions): HttpsCallable;
/**
* Changes this instance to point to a Cloud Functions emulator running

View File

@@ -15,7 +15,7 @@
*
*/
import { isAndroid } from '@react-native-firebase/app/lib/common';
import { isAndroid, isNumber } from '@react-native-firebase/app/lib/common';
import {
createModuleNamespace,
FirebaseModule,
@@ -59,11 +59,24 @@ class FirebaseFunctionsModule extends FirebaseModule {
this._useFunctionsEmulatorOrigin = null;
}
httpsCallable(name) {
httpsCallable(name, options = {}) {
if (options.timeout) {
if (isNumber(options.timeout)) {
options.timeout = options.timeout / 1000;
} else {
throw new Error('HttpsCallableOptions.timeout expected a Number in milliseconds');
}
}
return data => {
const nativePromise = this.native.httpsCallable(this._useFunctionsEmulatorOrigin, name, {
data,
});
const nativePromise = this.native.httpsCallable(
this._useFunctionsEmulatorOrigin,
name,
{
data,
},
options,
);
return nativePromise.catch(nativeError => {
const { code, message, details } = nativeError.userInfo || {};
return Promise.reject(