Skip to content

Commit

Permalink
BREAKING CHANGE! (WIP) Avoid dep on dart:io
Browse files Browse the repository at this point in the history
- AppConfiguration.baseFilePath is now a String instead of a Directory.
- AppConfiguration.httpClient is now a Client instead of a HttpClient.

This is to avoid a dependency on dart:io in order to be compatible with
package web.

This is still work in progress. Missing is:

- Support for timeout on web requests
- Custom certificate for windows.
  • Loading branch information
nielsenko committed Jun 2, 2024
1 parent 44c02ce commit f8f2403
Show file tree
Hide file tree
Showing 15 changed files with 83 additions and 123 deletions.
43 changes: 21 additions & 22 deletions packages/realm_dart/lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0

import 'dart:convert';
import 'dart:io';
import 'dart:isolate';

import 'package:http/http.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;

Expand Down Expand Up @@ -52,20 +52,20 @@ he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC
Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5
-----END CERTIFICATE-----''';

if (Platform.isWindows) {
try {
final context = SecurityContext(withTrustedRoots: true);
context.setTrustedCertificatesBytes(const AsciiEncoder().convert(isrgRootX1CertPEM));
return HttpClient(context: context);
} on TlsException catch (e) {
// certificate is already trusted. Nothing to do here
if (e.osError?.message.contains("CERT_ALREADY_IN_HASH_TABLE") != true) {
rethrow;
}
}
}

return HttpClient();
// if (Platform.isWindows) {
// try {
// final context = SecurityContext(withTrustedRoots: true);
// context.setTrustedCertificatesBytes(const AsciiEncoder().convert(isrgRootX1CertPEM));
// return Client(context: context);
// } on TlsException catch (e) {
// // certificate is already trusted. Nothing to do here
// if (e.osError?.message.contains("CERT_ALREADY_IN_HASH_TABLE") != true) {
// rethrow;
// }
// }
// }

return Client();
}();

/// A class exposing configuration options for an [App]
Expand All @@ -79,7 +79,7 @@ class AppConfiguration {
///
/// This data includes metadata for users and synchronized Realms. If set, you must ensure that the [baseFilePath]
/// directory exists.
final Directory baseFilePath;
final String baseFilePath;

/// The [baseUrl] is the [Uri] used to reach the MongoDB Atlas.
///
Expand All @@ -106,26 +106,26 @@ class AppConfiguration {
/// Setting this will not change the encryption key for individual Realms, which is set in the [Configuration].
final List<int>? metadataEncryptionKey;

/// The [HttpClient] that will be used for HTTP requests during authentication.
/// The [Client] that will be used for HTTP requests during authentication.
///
/// You can use this to override the default http client handler and configure settings like proxies,
/// client certificates, and cookies. While these are not required to connect to MongoDB Atlas under
/// normal circumstances, they can be useful if client devices are behind corporate firewall or use
/// a more complex networking setup.
final HttpClient httpClient;
final Client httpClient;

/// Instantiates a new [AppConfiguration] with the specified appId.
AppConfiguration(
this.appId, {
Uri? baseUrl,
Directory? baseFilePath,
String? baseFilePath,
this.defaultRequestTimeout = const Duration(seconds: 60),
this.metadataEncryptionKey,
this.metadataPersistenceMode = MetadataPersistenceMode.plaintext,
this.maxConnectionTimeout = const Duration(minutes: 2),
HttpClient? httpClient,
Client? httpClient,
}) : baseUrl = baseUrl ?? Uri.parse(realmCore.getDefaultBaseUrl()),
baseFilePath = baseFilePath ?? Directory(path.dirname(Configuration.defaultRealmPath)),
baseFilePath = baseFilePath ?? path.dirname(Configuration.defaultRealmPath),
httpClient = httpClient ?? _defaultClient {
if (appId == '') {
throw RealmException('Supplied appId must be a non-empty value');
Expand Down Expand Up @@ -170,7 +170,6 @@ class App {
App._(this._handle);

static AppHandle _createApp(AppConfiguration configuration) {
configuration.baseFilePath.createSync(recursive: true);
return AppHandle.from(configuration);
}

Expand Down
10 changes: 2 additions & 8 deletions packages/realm_dart/lib/src/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
// SPDX-License-Identifier: Apache-2.0

import 'dart:async';
import 'dart:io';

// ignore: no_leading_underscores_for_library_prefixes
import 'package:path/path.dart' as _path;

import 'app.dart';
import 'handles/realm_core.dart';
import 'init.dart';
import 'logging.dart';
import 'realm_class.dart';
import 'realm_dart.dart';
import 'user.dart';

const encryptionKeySize = 64;
Expand Down Expand Up @@ -84,11 +82,7 @@ abstract class Configuration {
/// On Flutter Linux this is the `/home/username/.local/share/app_name` directory.
/// On Dart standalone Windows, macOS and Linux this is the current directory.
static String get defaultStoragePath {
if (isFlutterPlatform) {
return realmCore.getAppDirectory();
}

return Directory.current.path;
return realmCore.getAppDirectory();
}

/// The platform dependent path to the default realm file.
Expand Down
6 changes: 4 additions & 2 deletions packages/realm_dart/lib/src/handles/native/app_handle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';

import '../../init.dart';
import 'init.dart';
import '../../realm_class.dart';
import 'convert.dart';
import 'convert_native.dart';
Expand Down Expand Up @@ -34,6 +34,8 @@ class AppHandle extends HandleBase<realm_app> implements intf.AppHandle {
_firstTime = false;
realmLib.realm_clear_cached_apps();
}
Directory(configuration.baseFilePath).createSync(recursive: true);

final httpTransportHandle = HttpTransportHandle.from(configuration.httpClient);
final appConfigHandle = _createAppConfig(configuration, httpTransportHandle);
return AppHandle(realmLib.realm_app_create_cached(appConfigHandle.pointer));
Expand Down Expand Up @@ -429,7 +431,7 @@ _AppConfigHandle _createAppConfig(AppConfiguration configuration, HttpTransportH

realmLib.realm_app_config_set_bundle_id(handle.pointer, realmCore.getBundleId().toCharPtr(arena));

realmLib.realm_app_config_set_base_file_path(handle.pointer, configuration.baseFilePath.path.toCharPtr(arena));
realmLib.realm_app_config_set_base_file_path(handle.pointer, configuration.baseFilePath.toCharPtr(arena));
realmLib.realm_app_config_set_metadata_mode(handle.pointer, configuration.metadataPersistenceMode.index);

if (configuration.metadataEncryptionKey != null && configuration.metadataPersistenceMode == MetadataPersistenceMode.encrypted) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import 'dart:convert';
import 'dart:ffi';
import 'dart:io';

import 'package:http/http.dart';

import '../../logging.dart';
import '../../realm_dart.dart';
import 'convert_native.dart';
Expand All @@ -17,7 +19,7 @@ import 'scheduler_handle.dart';
class HttpTransportHandle extends HandleBase<realm_http_transport> {
HttpTransportHandle(Pointer<realm_http_transport> pointer) : super(pointer, 24);

factory HttpTransportHandle.from(HttpClient httpClient) {
factory HttpTransportHandle.from(Client httpClient) {
final requestCallback = Pointer.fromFunction<Void Function(Handle, realm_http_request, Pointer<Void>)>(_requestCallback);
final requestCallbackUserdata = realmLib.realm_dart_userdata_async_new(httpClient, requestCallback.cast(), schedulerHandle.pointer);
return HttpTransportHandle(realmLib.realm_http_transport_new(
Expand All @@ -38,9 +40,9 @@ void _requestCallback(Object userData, realm_http_request request, Pointer<Void>
// We cannot clone request on the native side with realm_clone,
// since realm_http_request does not inherit from WrapC.

final client = userData as HttpClient;
final client = userData as Client;

client.connectionTimeout = Duration(milliseconds: request.timeout_ms);
// client.connectionTimeout = Duration(milliseconds: request.timeout_ms);

final url = Uri.parse(request.url.cast<Utf8>().toRealmDartString()!);

Expand All @@ -59,7 +61,7 @@ void _requestCallback(Object userData, realm_http_request request, Pointer<Void>
}

Future<void> _requestCallbackAsync(
HttpClient client,
Client client,
int requestMethod,
Uri url,
String? body,
Expand All @@ -73,67 +75,42 @@ Future<void> _requestCallbackAsync(

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;
}

final request = Request(method.name, url);
for (final header in headers.entries) {
request.headers.add(header.key, header.value);
request.headers[header.key] = header.value;
}

if (body != null) {
request.add(utf8.encode(body));
request.bodyBytes = utf8.encode(body);
}

Realm.logger.log(LogLevel.debug, "HTTP Transport: Executing ${method.name} $url");

final stopwatch = Stopwatch()..start();

// Do the call..
final response = await request.close();
final response = await client.send(request);

stopwatch.stop();
Realm.logger.log(LogLevel.debug, "HTTP Transport: Executed ${method.name} $url: ${response.statusCode} in ${stopwatch.elapsedMilliseconds} ms");

final responseBody = await response.fold<List<int>>([], (acc, l) => acc..addAll(l)); // gather response
final responseBody = await response.stream.fold<List<int>>([], (acc, l) => acc..addAll(l)); // gather response

// Report back to core
responseRef.status_code = response.statusCode;
responseRef.body = responseBody.toCharPtr(arena);
responseRef.body_size = responseBody.length;

int headerCnt = 0;
response.headers.forEach((name, values) {
headerCnt += values.length;
});

int headerCnt = response.headers.length;
responseRef.headers = arena<realm_http_header>(headerCnt);
responseRef.num_headers = headerCnt;

int index = 0;
response.headers.forEach((name, values) {
for (final value in values) {
final headerRef = (responseRef.headers + index).ref;
headerRef.name = name.toCharPtr(arena);
headerRef.value = value.toCharPtr(arena);
index++;
}
response.headers.forEach((name, value) {
final headerRef = (responseRef.headers + index).ref;
headerRef.name = name.toCharPtr(arena);
headerRef.value = value.toCharPtr(arena);
index++;
});

responseRef.custom_status_code = CustomErrorCode.noError.code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import 'dart:io';
import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as p;

import '../realm.dart' as realm show isFlutterPlatform;
import 'cli/common/target_os_type.dart';
import 'cli/metrics/metrics_command.dart';
import 'cli/metrics/options.dart';
import 'realm_class.dart';
import '../../cli/common/target_os_type.dart';
import '../../cli/metrics/metrics_command.dart';
import '../../cli/metrics/options.dart';
import '../../realm_class.dart';

import '../../../realm.dart' show isFlutterPlatform;
export '../../../realm.dart' show isFlutterPlatform;

const realmBinaryName = 'realm_dart';
final targetOsType = Platform.operatingSystem.asTargetOsType ?? _platformNotSupported();
Expand Down Expand Up @@ -50,7 +52,7 @@ String _getLibPathDart(Package realmDartPackage) {
_platformNotSupported();
}

bool get isFlutterPlatform => realm.isFlutterPlatform;
//bool get isFlutterPlatform => realm.isFlutterPlatform;

String _getLibName(String stem) => switch (targetOsType) {
TargetOsType.android => 'lib$stem.so',
Expand Down
6 changes: 5 additions & 1 deletion packages/realm_dart/lib/src/handles/native/realm_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import 'package:crypto/crypto.dart';
import 'package:path/path.dart' as path;
import 'package:pubspec_parse/pubspec_parse.dart';

import '../../init.dart';
import 'init.dart';
import '../../realm_class.dart';
import '../../scheduler.dart';
import 'convert_native.dart';
Expand Down Expand Up @@ -196,4 +196,8 @@ class RealmCore {
return outLimit.value;
});
}

bool checkIfRealmExists(String path) {
return File(path).existsSync(); // TODO: Should this not check that file is an actual realm file?
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import 'package:realm_common/realm_common.dart';

import '../../init.dart';
import 'init.dart';
import 'ffi.dart';
import 'realm_bindings.dart';

Expand Down
28 changes: 5 additions & 23 deletions packages/realm_dart/lib/src/realm_class.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import 'dart:async';
import 'dart:io';
import 'dart:isolate';

import 'package:cancellation_token/cancellation_token.dart';
import 'package:collection/collection.dart';
Expand Down Expand Up @@ -179,7 +179,7 @@ class Realm {
return await CancellableFuture.value(realm, cancellationToken);
}

_ensureDirectory(config);
// _ensureDirectory(config);

final asyncOpenHandle = AsyncOpenTaskHandle.from(config);
return await CancellableFuture.from<Realm>(() async {
Expand All @@ -205,17 +205,9 @@ class Realm {
}

static RealmHandle _openRealm(Configuration config) {
_ensureDirectory(config);
return RealmHandle.open(config);
}

static void _ensureDirectory(Configuration config) {
var dir = File(config.path).parent;
if (!dir.existsSync()) {
dir.createSync(recursive: true);
}
}

void _populateMetadata() {
schema = config.schemaObjects.isNotEmpty ? RealmSchema(config.schemaObjects) : handle.readSchema();
_metadata = RealmMetadata._(schema.map((c) => handle.getObjectMetadata(c)));
Expand All @@ -230,22 +222,12 @@ class Realm {

/// Synchronously checks whether a `Realm` exists at [path]
static bool existsSync(String path) {
try {
final fileEntity = File(path);
return fileEntity.existsSync();
} catch (e) {
throw RealmException("Error while checking if Realm exists at $path. Error: $e");
}
return realmCore.checkIfRealmExists(path);
}

/// Checks whether a `Realm` exists at [path].
static Future<bool> exists(String path) async {
try {
final fileEntity = File(path);
return await fileEntity.exists();
} catch (e) {
throw RealmException("Error while checking if Realm exists at $path. Error: $e");
}
return await Isolate.run(() => realmCore.checkIfRealmExists(path));
}

/// Adds a [RealmObject] to the `Realm`.
Expand Down Expand Up @@ -564,7 +546,7 @@ class Realm {
throw RealmException("Can't compact an in-memory Realm");
}

if (!File(config.path).existsSync()) {
if (!Realm.existsSync(config.path)) {
print("realm file doesn't exist: ${config.path}");
return false;
}
Expand Down
Loading

0 comments on commit f8f2403

Please sign in to comment.