Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RDART-866: Minimal web support #1699

Merged
merged 17 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -619,3 +619,30 @@ jobs:
*<https://github.com/realm/realm-dart/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}|_{{workflow}}_ run id: ${{ github.run_id }} has status _{{jobStatus}}_ >*
<{{refUrl}}|`{{ref}}` - {{description}}>
{{#if description}}<{{diffUrl}}|branch: `{{diffRef}}`>{{/if}}

web-compile:
name: Compile for web
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: "recursive"

- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"

- name: Setup Melos
run: |
dart pub global activate melos
dart pub global run melos bootstrap

- name: Compile to wasm
run: flutter build web --wasm -t integration_test/all_tests.dart
working-directory: packages/realm/tests/

- name: Compile to js
run: flutter build web -t integration_test/all_tests.dart
working-directory: packages/realm/tests/
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
## vNext (TBD)

### Breaking Changes
* To avoid dependency on `dart:io`
- `AppConfiguration.httpClient` is now of type [`Client`](https://pub.dev/documentation/http/latest/http/Client-class.html) and
- `AppConfiguration.baseFilePath` is now of type `String`

(Issue [#1374](https://github.com/realm/realm-dart/issues/1374))

### Enhancements
* Report the originating error that caused a client reset to occur. (Core 14.9.0)
* Allow the realm package, and code generated by realm_generator to be included when building
for web without breaking compilation. (Issue [#1374](https://github.com/realm/realm-dart/issues/1374))

### Fixed
* `Realm.writeAsync` did not handle async callbacks (`Future<T> Function()`) correctly. (Issue [#1667](https://github.com/realm/realm-dart/issues/1667))
Expand Down
5 changes: 3 additions & 2 deletions packages/realm/tests/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.1
test: any
timezone: ^0.9.0
integration_test:
sdk: flutter
test: any
timezone: ^0.9.0
universal_platform: ^1.1.0

flutter:
assets:
Expand Down
38 changes: 38 additions & 0 deletions packages/realm/tests/web/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.

The path provided below has to start and end with a slash "/" in order for
it to work correctly.

For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base

This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">

<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">

<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="tests">
<link rel="apple-touch-icon" href="icons/Icon-192.png">

<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>

<title>tests</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
35 changes: 35 additions & 0 deletions packages/realm/tests/web/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "tests",
"short_name": "tests",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
2 changes: 1 addition & 1 deletion packages/realm_dart/lib/realm.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2021 MongoDB, Inc.
// SPDX-License-Identifier: Apache-2.0

//dart.library.cli is available only on dart desktop
// dart.library.cli is available only on dart desktop
export 'src/realm_flutter.dart' if (dart.library.cli) 'src/realm_dart.dart';
export 'package:ejson/ejson.dart';
76 changes: 11 additions & 65 deletions packages/realm_dart/lib/src/app.dart
Original file line number Diff line number Diff line change
@@ -1,73 +1,20 @@
// Copyright 2022 MongoDB, Inc.
// 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;

import '../realm.dart';
import 'credentials.dart';
import 'handles/app_handle.dart';
import 'handles/default_client.dart';
import 'handles/realm_core.dart';
import 'logging.dart';
import 'native/app_handle.dart';
import 'native/realm_core.dart';
import 'user.dart';

final _defaultClient = () {
const isrgRootX1CertPEM = // The root certificate used by lets encrypt and hence MongoDB
'''
subject=CN=ISRG Root X1,O=Internet Security Research Group,C=US
issuer=CN=DST Root CA X3,O=Digital Signature Trust Co.
-----BEGIN CERTIFICATE-----
MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTIxMDEyMDE5MTQwM1oXDTI0MDkzMDE4MTQwM1ow
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQCt6CRz9BQ385ueK1coHIe+3LffOJCMbjzmV6B493XC
ov71am72AE8o295ohmxEk7axY/0UEmu/H9LqMZshftEzPLpI9d1537O4/xLxIZpL
wYqGcWlKZmZsj348cL+tKSIG8+TA5oCu4kuPt5l+lAOf00eXfJlII1PoOK5PCm+D
LtFJV4yAdLbaL9A4jXsDcCEbdfIwPPqPrt3aY6vrFk/CjhFLfs8L6P+1dy70sntK
4EwSJQxwjQMpoOFTJOwT2e4ZvxCzSow/iaNhUd6shweU9GNx7C7ib1uYgeGJXDR5
bHbvO5BieebbpJovJsXQEOEO3tkQjhb7t/eo98flAgeYjzYIlefiN5YNNnWe+w5y
sR2bvAP5SQXYgd0FtCrWQemsAXaVCg/Y39W9Eh81LygXbNKYwagJZHduRze6zqxZ
Xmidf3LWicUGQSk+WT7dJvUkyRGnWqNMQB9GoZm1pzpRboY7nn1ypxIFeFntPlF4
FQsDj43QLwWyPntKHEtzBRL8xurgUBN8Q5N0s8p0544fAQjQMNRbcTa0B7rBMDBc
SLeCO5imfWCKoqMpgsy6vYMEG6KDA0Gh1gXxG8K28Kh8hjtGqEgqiNx2mna/H2ql
PRmP6zjzZN7IKw0KKP/32+IVQtQi0Cdd4Xn+GOdwiK1O5tmLOsbdJ1Fu/7xk9TND
TwIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw
SwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5pZGVudHJ1
c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTEp7Gkeyxx
+tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEB
ATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQu
b3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0LmNvbS9E
U1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFHm0WeZ7tuXkAXOACIjIGlj26Ztu
MA0GCSqGSIb3DQEBCwUAA4IBAQAKcwBslm7/DlLQrt2M51oGrS+o44+/yQoDFVDC
5WxCu2+b9LRPwkSICHXM6webFGJueN7sJ7o5XPWioW5WlHAQU7G75K/QosMrAdSW
9MUgNTP52GE24HGNtLi1qoJFlcDyqSMo59ahy2cI2qBDLKobkx/J3vWraV0T9VuG
WCLKTVXkcGdtwlfFRjlBz4pYg1htmf5X6DYO8A4jqv2Il9DjXA6USbW1FzXSLr9O
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();
}();

/// A class exposing configuration options for an [App]
/// {@category Application}
@immutable
Expand All @@ -79,7 +26,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,27 +53,27 @@ 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)),
httpClient = httpClient ?? _defaultClient {
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 +117,6 @@ class App {
App._(this._handle);

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

Expand Down
2 changes: 1 addition & 1 deletion packages/realm_dart/lib/src/collections.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2022 MongoDB, Inc.
// SPDX-License-Identifier: Apache-2.0

import 'native/collection_changes_handle.dart';
import 'handles/collection_changes_handle.dart';

/// Contains index information about objects that moved within the same collection.
class Move {
Expand Down
48 changes: 32 additions & 16 deletions packages/realm_dart/lib/src/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +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 'init.dart';
import 'handles/realm_core.dart';
import 'logging.dart';
import 'native/from_native.dart';
import 'native/realm_core.dart';
import 'realm_class.dart';
import 'realm_dart.dart';
import 'user.dart';

const encryptionKeySize = 64;
Expand Down Expand Up @@ -85,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 Expand Up @@ -714,29 +707,52 @@ final class CompensatingWriteError extends SyncError {
}
}

/// @nodoc
class SyncErrorDetails {
final String message;
final SyncErrorCode code;
final String? path;
final bool isFatal;
final bool isClientResetRequested;
final String? originalFilePath;
final String? backupFilePath;
final List<CompensatingWriteInfo>? compensatingWrites;
final Object? userError;

SyncErrorDetails(
this.message,
this.code,
this.userError, {
this.path,
this.isFatal = false,
this.isClientResetRequested = false,
this.originalFilePath,
this.backupFilePath,
this.compensatingWrites,
});
}

/// @nodoc
extension SyncErrorInternal on SyncError {
static SyncError createSyncError(SyncErrorDetails error, {App? app}) {
//Client reset can be requested with isClientResetRequested disregarding the ErrorCode
SyncErrorCode errorCode = SyncErrorCode.fromInt(error.code);

return switch (errorCode) {
return switch (error.code) {
SyncErrorCode.autoClientResetFailed => ClientResetError._(
error.message,
errorCode,
error.code,
app,
error.userError,
originalFilePath: error.originalFilePath,
backupFilePath: error.backupFilePath,
),
SyncErrorCode.clientReset =>
ClientResetError._(error.message, errorCode, app, error.userError, originalFilePath: error.originalFilePath, backupFilePath: error.backupFilePath),
ClientResetError._(error.message, error.code, app, error.userError, originalFilePath: error.originalFilePath, backupFilePath: error.backupFilePath),
SyncErrorCode.compensatingWrite => CompensatingWriteError._(
error.message,
error.userError,
compensatingWrites: error.compensatingWrites,
),
_ => SyncError._(error.message, errorCode, error.userError),
_ => SyncError._(error.message, error.code, error.userError),
};
}
}
8 changes: 8 additions & 0 deletions packages/realm_dart/lib/src/convert.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

extension NullableObjectEx<T> on T? {
U? convert<U>(U Function(T) convertor) {
final self = this;
if (self == null) return null;
return convertor(self);
}
}
4 changes: 2 additions & 2 deletions packages/realm_dart/lib/src/credentials.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import 'dart:convert';

import 'app.dart';
import 'native/convert.dart';
import 'native/credentials_handle.dart';
import 'convert.dart';
import 'handles/credentials_handle.dart';
import 'user.dart';

/// An enum containing all authentication providers. These have to be enabled manually for the application before they can be used.
Expand Down
Loading
Loading