Skip to content

Commit

Permalink
feat(functions, web): migrate web to js_interop to be compatible with…
Browse files Browse the repository at this point in the history
… WASM (#12205)

* feat(functions, web): migrate web to js_interop to be compatible with WASM

* clean interop

* tests thanks to Russell :)
  • Loading branch information
Lyokone committed Feb 27, 2024
1 parent 6c1f73d commit 51f6563
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:js_util' as util;
import 'dart:js_interop';

import 'package:cloud_functions_platform_interface/cloud_functions_platform_interface.dart';

Expand All @@ -21,7 +21,7 @@ class HttpsCallableWeb extends HttpsCallablePlatform {
final functions_interop.Functions _webFunctions;

@override
Future<dynamic> call([dynamic parameters]) async {
Future<dynamic> call([Object? parameters]) async {
if (origin != null) {
final uri = Uri.parse(origin!);

Expand All @@ -30,8 +30,8 @@ class HttpsCallableWeb extends HttpsCallablePlatform {

functions_interop.HttpsCallableOptions callableOptions =
functions_interop.HttpsCallableOptions(
timeout: options.timeout.inMilliseconds,
limitedUseAppCheckTokens: options.limitedUseAppCheckToken,
timeout: options.timeout.inMilliseconds.toJS,
limitedUseAppCheckTokens: options.limitedUseAppCheckToken.toJS,
);

late functions_interop.HttpsCallable callable;
Expand All @@ -45,15 +45,13 @@ class HttpsCallableWeb extends HttpsCallablePlatform {
}

functions_interop.HttpsCallableResult response;
var input = parameters;
if ((input is Map) || (input is Iterable)) {
input = util.jsify(parameters);
}

final JSAny? parametersJS = parameters?.jsify();

try {
response = await callable.call(input);
response = await callable.call(parametersJS);
} catch (e, s) {
throw convertFirebaseFunctionsException(e, s);
throw convertFirebaseFunctionsException(e as JSObject, s);
}

return response.data;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

// ignore_for_file: public_member_api_docs

import 'dart:js_interop';

import 'package:firebase_core_web/firebase_core_web_interop.dart';

import 'functions_interop.dart' as functions_interop;
Expand All @@ -13,12 +15,10 @@ export 'functions_interop.dart' show HttpsCallableOptions;

/// Given an AppJSImp, return the Functions instance.
Functions getFunctionsInstance(App app, [String? region]) {
functions_interop.FunctionsJsImpl jsObject;
if (region == null) {
jsObject = functions_interop.getFunctions(app.jsObject);
} else {
jsObject = functions_interop.getFunctions(app.jsObject, region);
}
functions_interop.FunctionsJsImpl jsObject = functions_interop.getFunctions(
app.jsObject,
region?.toJS,
);
return Functions.getInstance(jsObject);
}

Expand All @@ -38,56 +38,88 @@ class Functions extends JsObjectWrapper<functions_interop.FunctionsJsImpl> {

HttpsCallable httpsCallable(String name,
[functions_interop.HttpsCallableOptions? options]) {
functions_interop.CustomFunction httpCallableImpl;
JSFunction httpCallableImpl;
if (options != null) {
httpCallableImpl =
functions_interop.httpsCallable(jsObject, name, options);
functions_interop.httpsCallable(jsObject, name.toJS, options);
} else {
httpCallableImpl = functions_interop.httpsCallable(jsObject, name);
httpCallableImpl = functions_interop.httpsCallable(jsObject, name.toJS);
}
return HttpsCallable.getInstance(httpCallableImpl);
}

HttpsCallable httpsCallableUri(Uri uri,
[functions_interop.HttpsCallableOptions? options]) {
functions_interop.CustomFunction httpCallableImpl;
JSFunction httpCallableImpl;
if (options != null) {
httpCallableImpl = functions_interop.httpsCallableFromURL(
jsObject, uri.toString(), options);
jsObject, uri.toString().toJS, options);
} else {
httpCallableImpl =
functions_interop.httpsCallableFromURL(jsObject, uri.toString());
functions_interop.httpsCallableFromURL(jsObject, uri.toString().toJS);
}
return HttpsCallable.getInstance(httpCallableImpl);
}

void useFunctionsEmulator(String host, int port) =>
functions_interop.connectFunctionsEmulator(jsObject, host, port);
void useFunctionsEmulator(String host, int port) => functions_interop
.connectFunctionsEmulator(jsObject, host.toJS, port.toJS);
}

class HttpsCallable extends JsObjectWrapper<functions_interop.CustomFunction> {
HttpsCallable._fromJsObject(functions_interop.CustomFunction jsObject)
class HttpsCallable extends JsObjectWrapper<JSFunction> {
HttpsCallable._fromJsObject(JSFunction jsObject)
: super.fromJsObject(jsObject);

static final _expando = Expando<HttpsCallable>();

/// Creates a new HttpsCallable from a [jsObject].
static HttpsCallable getInstance(functions_interop.CustomFunction jsObject) {
static HttpsCallable getInstance(JSFunction jsObject) {
return _expando[jsObject] ??= HttpsCallable._fromJsObject(jsObject);
}

Future<HttpsCallableResult> call([dynamic data]) =>
handleThenable(jsObject.apply(null, [data])).then((result) {
return HttpsCallableResult.getInstance(
result as functions_interop.HttpsCallableResultJsImpl);
});
Future<HttpsCallableResult> call(JSAny? data) async {
final result =
await (jsObject.callAsFunction(null, data)! as JSPromise).toDart;

return HttpsCallableResult.getInstance(
result! as functions_interop.HttpsCallableResultJsImpl,
);
}
}

/// Returns Dart representation from JS Object.
dynamic _dartify(dynamic object) {
// Convert JSObject to Dart equivalents directly
if (object is! JSObject) {
return object;
}

final jsObject = object;

// Convert nested structures
final dartObject = jsObject.dartify();
return _convertNested(dartObject);
}

dynamic _convertNested(dynamic object) {
if (object is List) {
return object.map(_convertNested).toList();
} else if (object is Map) {
var map = <String, dynamic>{};
object.forEach((key, value) {
map[key] = _convertNested(value);
});
return map;
} else {
// For non-nested types, attempt to convert directly
return dartify(object);
}
}

class HttpsCallableResult
extends JsObjectWrapper<functions_interop.HttpsCallableResultJsImpl> {
HttpsCallableResult._fromJsObject(
functions_interop.HttpsCallableResultJsImpl jsObject)
: _data = dartify(jsObject.data),
: _data = _dartify(jsObject.data),
super.fromJsObject(jsObject);

static final _expando = Expando<HttpsCallableResult>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,90 +8,71 @@
@JS('firebase_functions')
library firebase_interop.functions;

import 'dart:js_interop';

import 'package:firebase_core_web/firebase_core_web_interop.dart';
import 'package:js/js.dart';

@JS()
external FunctionsJsImpl getFunctions([AppJsImpl? app, String? regionOrDomain]);
@staticInterop
external FunctionsJsImpl getFunctions(
[AppJsImpl? app, JSString? regionOrDomain]);

@JS()
@staticInterop
external void connectFunctionsEmulator(
FunctionsJsImpl functions, String host, int port);
FunctionsJsImpl functions, JSString host, JSNumber port);

@JS()
external CustomFunction httpsCallable(FunctionsJsImpl functions, String name,
@staticInterop
external JSFunction httpsCallable(FunctionsJsImpl functions, JSString name,
[HttpsCallableOptions? options]);

@JS()
external CustomFunction httpsCallableFromURL(
FunctionsJsImpl functions, String url,
@staticInterop
external JSFunction httpsCallableFromURL(
FunctionsJsImpl functions, JSString url,
[HttpsCallableOptions? options]);

/// The Cloud Functions for Firebase service interface.
///
/// Do not call this constructor directly. Instead, use firebase.functions().
/// See: <https://firebase.google.com/docs/reference/js/firebase.functions.Functions>.
@JS('Functions')
abstract class FunctionsJsImpl {
@staticInterop
abstract class FunctionsJsImpl {}

extension FunctionsJsImplExtension on FunctionsJsImpl {
external AppJsImpl get app;
external String? get customDomain;
external String get region;
external JSString? get customDomain;
external JSString get region;
}

/// An HttpsCallableOptions is an option to set timeout property
///
/// See: <https://firebase.google.com/docs/reference/js/firebase.functions.HttpsCallableOptions>.
@JS('HttpsCallableOptions')
@staticInterop
@anonymous
abstract class HttpsCallableOptions {
external factory HttpsCallableOptions(
{int? timeout, bool? limitedUseAppCheckTokens});
external int get timeout;
external set timeout(int t);
external bool get limitedUseAppCheckTokens;
external set limitedUseAppCheckTokens(bool t);
{JSNumber? timeout, JSBoolean? limitedUseAppCheckTokens});
}

extension HttpsCallableOptionsExtension on HttpsCallableOptions {
external JSNumber? get timeout;
external set timeout(JSNumber? t);
external JSBoolean? get limitedUseAppCheckTokens;
external set limitedUseAppCheckTokens(JSBoolean? t);
}

/// An HttpsCallableResult wraps a single result from a function call.
///
/// See: <https://firebase.google.com/docs/reference/js/firebase.functions.HttpsCallableResult>.
/// See: <https://firebase.google.com/docs/reference/js/functions.httpscallableresult>.
@JS('HttpsCallableResult')
@staticInterop
@anonymous
abstract class HttpsCallableResultJsImpl {
external Map<String, dynamic> get data;
}

/// The set of Cloud Functions status codes.
/// These status codes are also exposed by gRPC.
///
/// See: <https://firebase.google.com/docs/reference/js/firebase.functions.HttpsError>.
@JS('HttpsError')
abstract class HttpsErrorJsImpl {
external ErrorJsImpl get error;
external set error(ErrorJsImpl e);
external String get code;
external set code(String v);
external dynamic get details;
external set details(dynamic d);
external String get message;
external set message(String v);
external String get name;
external set name(String v);
external String get stack;
external set stack(String s);
}

@JS('Error')
abstract class ErrorJsImpl {
external String get message;
external set message(String m);
external String get fileName;
external set fileName(String f);
external String get lineNumber;
external set lineNumber(String l);
}
abstract class HttpsCallableResultJsImpl {}

@JS('Function')
class CustomFunction {
external PromiseJsImpl<dynamic> apply(dynamic thisArg, List<dynamic> args);
extension HttpsCallableResultJsImplExtension on HttpsCallableResultJsImpl {
external JSAny? get data;
}
22 changes: 12 additions & 10 deletions packages/cloud_functions/cloud_functions_web/lib/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,25 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:js_util' as util;
import 'dart:js_interop';
import 'dart:js_interop_unsafe';

import 'package:cloud_functions_platform_interface/cloud_functions_platform_interface.dart';
import 'package:firebase_core_web/firebase_core_web_interop.dart' show dartify;

/// Given a web error, a [FirebaseFunctionsException] is returned.
FirebaseFunctionsException convertFirebaseFunctionsException(Object exception,
FirebaseFunctionsException convertFirebaseFunctionsException(JSObject exception,
[StackTrace? stackTrace]) {
String originalCode = util.getProperty(exception, 'code');
String originalCode =
(exception.getProperty('code'.toJS)! as JSString).toDart;
String code = originalCode.replaceFirst('functions/', '');
String message = util
.getProperty(exception, 'message')
String message = (exception.getProperty('message'.toJS)! as JSString)
.toDart
.replaceFirst('($originalCode)', '');

return FirebaseFunctionsException(
code: code,
message: message,
stackTrace: stackTrace,
details: dartify(util.getProperty(exception, 'details')));
code: code,
message: message,
stackTrace: stackTrace,
details: exception.getProperty('details'.toJS)?.dartify(),
);
}
3 changes: 2 additions & 1 deletion packages/cloud_functions/cloud_functions_web/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ repository: https://github.com/firebase/flutterfire/tree/master/packages/cloud_f
version: 4.6.16

environment:
sdk: '>=2.18.0 <4.0.0'
sdk: '>=3.2.0 <4.0.0'
flutter: '>=3.3.0'

dependencies:
Expand All @@ -18,6 +18,7 @@ dependencies:
flutter_web_plugins:
sdk: flutter
js: ^0.6.3
web: '>=0.3.0 <0.5.0'

dev_dependencies:
firebase_core_platform_interface: ^5.0.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,12 +230,15 @@ void main() {

test(
'allow passing of `limitedUseAppCheckToken` as option',
() async {
() async {
final instance = FirebaseFunctions.instance;
instance.useFunctionsEmulator('localhost', 5001);
final timeoutCallable = FirebaseFunctions.instance.httpsCallable(
kTestFunctionDefaultRegion,
options: HttpsCallableOptions(timeout: const Duration(seconds: 3), limitedUseAppCheckToken: true),
options: HttpsCallableOptions(
timeout: const Duration(seconds: 3),
limitedUseAppCheckToken: true,
),
);

HttpsCallableResult results = await timeoutCallable(null);
Expand Down

0 comments on commit 51f6563

Please sign in to comment.