diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index c31e9c5fe4..12781b4055 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -20,6 +20,8 @@ import 'dart:convert'; import 'dart:ffi'; +import 'dart:ffi' as ffi show Handle; +import 'dart:io'; import 'dart:typed_data'; // Hide StringUtf8Pointer.toNativeUtf8 and StringUtf16Pointer since these allows silently allocating memory. Use toUtf8Ptr instead @@ -38,6 +40,48 @@ late RealmLibrary _realmLib; final _RealmCore realmCore = _RealmCore(); +// TODO: Once enhanced-enums land in 2.17, replace with: +/* +enum _CustomErrorCode { + noError(0), + httpClientDisposed(997), + unknownHttp(998), + unknown(999), + timeout(1000); + + final int code; + const _CustomErrorCode(this.code); +} +*/ + +enum _CustomErrorCode { + noError, + httpClientDisposed, + unknownHttp, + unknown, + timeout, +} + +extension on _CustomErrorCode { + int get code { + switch (this) { + case _CustomErrorCode.noError: return 0; + case _CustomErrorCode.httpClientDisposed: return 997; + case _CustomErrorCode.unknownHttp: return 998; + case _CustomErrorCode.unknown: return 999; + case _CustomErrorCode.timeout: return 1000; + } + } +} + +enum _HttpMethod { + get, + post, + patch, + put, + delete, +} + class _RealmCore { // From realm.h. Currently not exported from the shared library static const int RLM_INVALID_CLASS_KEY = 0x7FFFFFFF; @@ -180,6 +224,123 @@ class _RealmCore { }); } + RealmHttpTransportHandle createHttpTransport(HttpClient httpClient) { + return RealmHttpTransportHandle._(_realmLib.realm_http_transport_new( + Pointer.fromFunction(request_callback), + _realmLib.gc_handle_weak_new(httpClient), + nullptr, + )); + } + + static void request_callback(Pointer userData, realm_http_request request, Pointer request_context) { + // + // The request struct only survives until end-of-call, even though + // we explicitly call realm_http_transport_complete_request to + // mark request as completed later. + // + // Therefor we need to copy everything out of request before returning. + // We cannot clone request on the native side with realm_clone, + // since realm_http_request does not inherit from WrapC. + // + final client = _realmLib.gc_handle_deref(userData) as HttpClient; + client.connectionTimeout = Duration(milliseconds: request.timeout_ms); + + final url = Uri.parse(request.url.cast().toDartString()); + + final method = _HttpMethod.values[request.method]; + + final body = request.body.cast().toDartString(); + + final headers = {}; + for (int i = 0; i < request.num_headers; ++i) { + final header = request.headers[i]; + final name = header.name.cast().toDartString(); + final value = header.value.cast().toDartString(); + headers[name] = value; + } + + _request_callback_async(client, method, url, body, headers, request_context); + // The request struct dies here! + } + + static void _request_callback_async( + HttpClient client, + _HttpMethod method, + Uri url, + String body, + Map headers, + Pointer request_context, + ) async { + await using((arena) async { + final response_pointer = arena(); + final responseRef = response_pointer.ref; + try { + // Build request + late HttpClientRequest request; + switch (method) { + case _HttpMethod.delete: + request = await client.deleteUrl(url); + break; + case _HttpMethod.put: + request = await client.putUrl(url); + break; + case _HttpMethod.patch: + request = await client.patchUrl(url); + break; + case _HttpMethod.post: + request = await client.postUrl(url); + break; + case _HttpMethod.get: + request = await client.getUrl(url); + break; + } + + for (final header in headers.entries) { + request.headers.add(header.key, header.value); + } + + request.add(utf8.encode(body)); + + // Do the call.. + final response = await request.close(); + final responseBody = await response.fold>([], (acc, l) => acc..addAll(l)); // gather response + + // Report back to core + responseRef.status_code = response.statusCode; + responseRef.body = responseBody.toInt8Ptr(arena); + responseRef.body_size = responseBody.length; + + int headerCnt = 0; + response.headers.forEach((name, values) { + headerCnt += values.length; + }); + + responseRef.headers = arena(headerCnt); + responseRef.num_headers = headerCnt; + + response.headers.forEach((name, values) { + int idx = 0; + for (final value in values) { + final headerRef = responseRef.headers.elementAt(idx).ref; + headerRef.name = name.toUtf8Ptr(arena); + headerRef.value = value.toUtf8Ptr(arena); + } + }); + + responseRef.custom_status_code = _CustomErrorCode.noError.code; + } on SocketException catch (_) { + // TODO: A Timeout causes a socket exception, but not all socket exceptions are due to timeouts + responseRef.custom_status_code = _CustomErrorCode.timeout.code; + } on HttpException catch (_) { + responseRef.custom_status_code = _CustomErrorCode.unknownHttp.code; + } catch (_) { + responseRef.custom_status_code = _CustomErrorCode.unknown.code; + } finally { + _realmLib.realm_http_transport_complete_request(request_context, response_pointer); + } + }); + } + ConfigHandle createConfig() { final configPtr = _realmLib.realm_config_new(); return ConfigHandle._(configPtr); @@ -752,15 +913,25 @@ class RealmAppCredentialsHandle extends Handle { RealmAppCredentialsHandle._(Pointer pointer) : super(pointer, 16); } +class RealmHttpTransportHandle extends Handle { + RealmHttpTransportHandle._(Pointer pointer) : super(pointer, 256); // TODO; What should hint be? +} + +extension on List { + Pointer toInt8Ptr(Allocator allocator) { + final nativeSize = length + 1; + final result = allocator(nativeSize); + final Uint8List native = result.asTypedList(nativeSize); + native.setAll(0, this); // copy + native.last = 0; // zero terminate + return result.cast(); + } +} + extension _StringEx on String { Pointer toUtf8Ptr(Allocator allocator) { final units = utf8.encode(this); - final nativeStringSize = units.length + 1; - final result = allocator(nativeStringSize); - final Uint8List nativeString = result.asTypedList(nativeStringSize); - nativeString.setAll(0, units); // copy to native string - nativeString.last = 0; // zero terminate - return result.cast(); + return units.toInt8Ptr(allocator); } Pointer toRealmString(Allocator allocator) { diff --git a/src/realm_dart.cpp b/src/realm_dart.cpp index a2157c2a6c..acca4ee919 100644 --- a/src/realm_dart.cpp +++ b/src/realm_dart.cpp @@ -70,6 +70,7 @@ void dummy(void) { realm_results_add_notification_callback(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); realm_results_snapshot(nullptr); realm_app_credentials_new_anonymous(); + realm_http_transport_new(nullptr, nullptr, nullptr); #if (ANDROID) realm_android_dummy(); #endif