Skip to content

Commit 7dd2231

Browse files
authored
Expose User.changes (#1500)
* Expose User.changes * Update core * Update Core to master * Fix build * Fix changelog versions
1 parent 7397eb5 commit 7dd2231

File tree

9 files changed

+201
-17
lines changed

9 files changed

+201
-17
lines changed

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
## vNext (TBD)
22

33
### Enhancements
4-
* None
4+
* Added `User.changes` stream that allows subscribers to receive notifications when the User changes - for example when the user's custom data changes or when their authentication state changes. (PR [#1500](https://github.com/realm/realm-dart/pull/1500))
5+
* Allow the query builder to construct >, >=, <, <= queries for string constants. This is a case sensitive lexicographical comparison. Improved performance of RQL queries on a non-linked string property using: >, >=, <, <=, operators and fixed behaviour that a null string should be evaulated as less than everything, previously nulls were not matched. (Core 13.26.0-13-gd12c3)
56

67
### Fixed
78
* Creating an `AppConfiguration` with an empty appId will now throw an exception rather than crashing the app. (Issue [#1487](https://github.com/realm/realm-dart/issues/1487))
9+
* Uploading the changesets recovered during an automatic client reset recovery may lead to 'Bad server version' errors and a new client reset. (Core 13.26.0-13-gd12c3)
810

911
### Compatibility
1012
* Realm Studio: 13.0.0 or later.
1113

1214
### Internal
13-
* Using Core x.y.z.
15+
* Using Core 13.26.0-13-gd12c3
1416

1517
## 1.8.0 (2024-01-29)
1618

lib/src/native/realm_bindings.dart

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4019,6 +4019,24 @@ class RealmLibrary {
40194019
_realm_dart_sync_wait_for_completion_callbackPtr.asFunction<
40204020
void Function(ffi.Pointer<ffi.Void>, ffi.Pointer<realm_error_t>)>();
40214021

4022+
void realm_dart_user_change_callback(
4023+
ffi.Pointer<ffi.Void> userdata,
4024+
int state,
4025+
) {
4026+
return _realm_dart_user_change_callback(
4027+
userdata,
4028+
state,
4029+
);
4030+
}
4031+
4032+
late final _realm_dart_user_change_callbackPtr = _lookup<
4033+
ffi
4034+
.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>, ffi.Int32)>>(
4035+
'realm_dart_user_change_callback');
4036+
late final _realm_dart_user_change_callback =
4037+
_realm_dart_user_change_callbackPtr
4038+
.asFunction<void Function(ffi.Pointer<ffi.Void>, int)>();
4039+
40224040
void realm_dart_user_completion_callback(
40234041
ffi.Pointer<ffi.Void> userdata,
40244042
ffi.Pointer<realm_user_t> user,
@@ -10872,6 +10890,38 @@ class RealmLibrary {
1087210890
realm_timestamp_t Function(
1087310891
ffi.Pointer<realm_flx_sync_subscription_t>)>();
1087410892

10893+
/// @return a notification token object. Dispose it to stop receiving notifications.
10894+
ffi.Pointer<realm_sync_user_subscription_token_t>
10895+
realm_sync_user_on_state_change_register_callback(
10896+
ffi.Pointer<realm_user_t> arg0,
10897+
realm_sync_on_user_state_changed_t arg1,
10898+
ffi.Pointer<ffi.Void> userdata,
10899+
realm_free_userdata_func_t userdata_free,
10900+
) {
10901+
return _realm_sync_user_on_state_change_register_callback(
10902+
arg0,
10903+
arg1,
10904+
userdata,
10905+
userdata_free,
10906+
);
10907+
}
10908+
10909+
late final _realm_sync_user_on_state_change_register_callbackPtr = _lookup<
10910+
ffi.NativeFunction<
10911+
ffi.Pointer<realm_sync_user_subscription_token_t> Function(
10912+
ffi.Pointer<realm_user_t>,
10913+
realm_sync_on_user_state_changed_t,
10914+
ffi.Pointer<ffi.Void>,
10915+
realm_free_userdata_func_t)>>(
10916+
'realm_sync_user_on_state_change_register_callback');
10917+
late final _realm_sync_user_on_state_change_register_callback =
10918+
_realm_sync_user_on_state_change_register_callbackPtr.asFunction<
10919+
ffi.Pointer<realm_sync_user_subscription_token_t> Function(
10920+
ffi.Pointer<realm_user_t>,
10921+
realm_sync_on_user_state_changed_t,
10922+
ffi.Pointer<ffi.Void>,
10923+
realm_free_userdata_func_t)>();
10924+
1087510925
/// Update the schema of an open realm.
1087610926
///
1087710927
/// This is equivalent to calling `realm_update_schema_advanced(realm, schema, 0,
@@ -11377,6 +11427,11 @@ class _SymbolAddresses {
1137711427
ffi.Pointer<ffi.Void>, ffi.Pointer<realm_error_t>)>>
1137811428
get realm_dart_sync_wait_for_completion_callback =>
1137911429
_library._realm_dart_sync_wait_for_completion_callbackPtr;
11430+
ffi.Pointer<
11431+
ffi
11432+
.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>, ffi.Int32)>>
11433+
get realm_dart_user_change_callback =>
11434+
_library._realm_dart_user_change_callbackPtr;
1138011435
ffi.Pointer<
1138111436
ffi.NativeFunction<
1138211437
ffi.Void Function(ffi.Pointer<ffi.Void>,
@@ -12488,6 +12543,12 @@ typedef realm_sync_on_subscription_state_changed_tFunction = ffi.Void Function(
1248812543
ffi.Pointer<ffi.Void> userdata, ffi.Int32 state);
1248912544
typedef Dartrealm_sync_on_subscription_state_changed_tFunction = void Function(
1249012545
ffi.Pointer<ffi.Void> userdata, int state);
12546+
typedef realm_sync_on_user_state_changed_t = ffi
12547+
.Pointer<ffi.NativeFunction<realm_sync_on_user_state_changed_tFunction>>;
12548+
typedef realm_sync_on_user_state_changed_tFunction = ffi.Void Function(
12549+
ffi.Pointer<ffi.Void> userdata, ffi.Int32 s);
12550+
typedef Dartrealm_sync_on_user_state_changed_tFunction = void Function(
12551+
ffi.Pointer<ffi.Void> userdata, int s);
1249112552

1249212553
abstract class realm_sync_progress_direction {
1249312554
static const int RLM_SYNC_PROGRESS_DIRECTION_UPLOAD = 0;
@@ -12635,6 +12696,11 @@ typedef Dartrealm_sync_ssl_verify_func_tFunction = bool Function(
1263512696
int preverify_ok,
1263612697
int depth);
1263712698

12699+
final class realm_sync_user_subscription_token extends ffi.Opaque {}
12700+
12701+
typedef realm_sync_user_subscription_token_t
12702+
= realm_sync_user_subscription_token;
12703+
1263812704
/// Callback function invoked by the sync session once it has uploaded or download
1263912705
/// all available changesets. See @a realm_sync_session_wait_for_upload and
1264012706
/// @a realm_sync_session_wait_for_download.

lib/src/native/realm_core.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,6 +1735,12 @@ class _RealmCore {
17351735
}
17361736
}
17371737

1738+
static void user_change_callback(Pointer<Void> userdata, int data) {
1739+
final controller = userdata as UserNotificationsController;
1740+
1741+
controller.onUserChanged();
1742+
}
1743+
17381744
RealmNotificationTokenHandle subscribeResultsNotifications(RealmResults results, NotificationsController controller) {
17391745
final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_results_add_notification_callback(
17401746
results.handle._pointer,
@@ -1783,6 +1789,18 @@ class _RealmCore {
17831789
return RealmNotificationTokenHandle._(pointer, map.realm.handle);
17841790
}
17851791

1792+
UserNotificationTokenHandle subscribeUserNotifications(UserNotificationsController controller) {
1793+
final callback = Pointer.fromFunction<Void Function(Handle, Int32)>(user_change_callback);
1794+
final userdata = _realmLib.realm_dart_userdata_async_new(controller, callback.cast(), scheduler.handle._pointer);
1795+
final notification_token = _realmLib.realm_sync_user_on_state_change_register_callback(
1796+
controller.user.handle._pointer,
1797+
_realmLib.addresses.realm_dart_user_change_callback,
1798+
userdata.cast(),
1799+
_realmLib.addresses.realm_dart_userdata_async_free,
1800+
);
1801+
return UserNotificationTokenHandle._(notification_token);
1802+
}
1803+
17861804
bool getObjectChangesIsDeleted(RealmObjectChangesHandle handle) {
17871805
return _realmLib.realm_object_changes_is_deleted(handle._pointer);
17881806
}
@@ -3184,6 +3202,10 @@ class RealmNotificationTokenHandle extends RootedHandleBase<realm_notification_t
31843202
RealmNotificationTokenHandle._(Pointer<realm_notification_token> pointer, RealmHandle root) : super(root, pointer, 32);
31853203
}
31863204

3205+
class UserNotificationTokenHandle extends HandleBase<realm_sync_user_subscription_token> {
3206+
UserNotificationTokenHandle._(Pointer<realm_sync_user_subscription_token> pointer) : super(pointer, 32);
3207+
}
3208+
31873209
class RealmSyncSessionConnectionStateNotificationTokenHandle extends HandleBase<realm_sync_session_connection_state_notification_token> {
31883210
RealmSyncSessionConnectionStateNotificationTokenHandle._(Pointer<realm_sync_session_connection_state_notification_token> pointer) : super(pointer, 32);
31893211
}

lib/src/realm_class.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export 'session.dart'
148148
// ignore: deprecated_member_use_from_same_package
149149
SyncSessionErrorCode;
150150
export 'subscription.dart' show Subscription, SubscriptionSet, SubscriptionSetState, MutableSubscriptionSet;
151-
export 'user.dart' show User, UserState, ApiKeyClient, UserIdentity, ApiKey, FunctionsClient;
151+
export 'user.dart' show User, UserState, ApiKeyClient, UserIdentity, ApiKey, FunctionsClient, UserChanges;
152152
export 'native/realm_core.dart' show Decimal128;
153153

154154
/// A [Realm] instance represents a `Realm` database.

lib/src/user.dart

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,24 @@
1616
//
1717
////////////////////////////////////////////////////////////////////////////////
1818
19+
import 'dart:async';
1920
import 'dart:convert';
21+
import 'dart:ffi';
2022

2123
import 'native/realm_core.dart';
2224
import 'realm_class.dart';
2325
import './app.dart';
2426

27+
/// Describes the changes to a [User] instance - for example when the access token is updated or the user state changes.
28+
/// Right now, this only conveys information that the user has changed, but in the future it will be enhanced by adding
29+
/// details about the exact properties that have been updated.
30+
class UserChanges {
31+
/// The user that has changed.
32+
final User user;
33+
34+
const UserChanges._(this.user);
35+
}
36+
2537
/// This class represents a `user` in an [Atlas App Services](https://www.mongodb.com/docs/atlas/app-services/) application.
2638
/// A user can log in to the server and, if access is granted, it is possible to synchronize the local Realm to MongoDB Atlas.
2739
/// Moreover, synchronization is halted when the user is logged out. It is possible to persist a user. By retrieving a user, there is no need to log in again.
@@ -161,6 +173,55 @@ class User {
161173
throw RealmError('User must be logged in to $clarification');
162174
}
163175
}
176+
177+
/// Gets a [Stream] of [UserChanges] that can be used to receive notifications when the user changes.
178+
Stream<UserChanges> get changes {
179+
final controller = UserNotificationsController(this);
180+
return controller.createStream();
181+
}
182+
}
183+
184+
/// @nodoc
185+
class UserNotificationsController implements Finalizable {
186+
UserNotificationTokenHandle? tokenHandle;
187+
188+
void start() {
189+
if (tokenHandle != null) {
190+
throw RealmStateError("User notifications subscription already started");
191+
}
192+
193+
tokenHandle = realmCore.subscribeUserNotifications(this);
194+
}
195+
196+
void stop() {
197+
// If handle is null or released, no-op
198+
if (tokenHandle?.released != false) {
199+
return;
200+
}
201+
202+
tokenHandle!.release();
203+
tokenHandle = null;
204+
}
205+
206+
User user;
207+
208+
late final StreamController<UserChanges> streamController;
209+
210+
UserNotificationsController(this.user);
211+
212+
Stream<UserChanges> createStream() {
213+
streamController = StreamController<UserChanges>(onListen: start, onCancel: stop);
214+
return streamController.stream;
215+
}
216+
217+
void onUserChanged() {
218+
final changes = UserChanges._(user);
219+
streamController.add(changes);
220+
}
221+
222+
void onError(RealmError error) {
223+
streamController.addError(error);
224+
}
164225
}
165226

166227
/// The current state of a [User].

src/realm_dart_sync.cpp

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,15 @@ RLM_API void realm_dart_sync_connection_state_changed_callback(realm_userdata_t
173173
});
174174
}
175175

176+
177+
RLM_API void realm_dart_user_change_callback(realm_userdata_t userdata, realm_user_state_e state)
178+
{
179+
auto ud = reinterpret_cast<realm_dart_userdata_async_t>(userdata);
180+
ud->scheduler->invoke([ud, state]() {
181+
(reinterpret_cast<realm_sync_on_user_state_changed_t>(ud->dart_callback))(ud->handle, state);
182+
});
183+
}
184+
176185
RLM_API void realm_dart_sync_on_subscription_state_changed_callback(realm_userdata_t userdata, realm_flx_sync_subscription_set_state_e state)
177186
{
178187
auto ud = reinterpret_cast<realm_dart_userdata_async_t>(userdata);
@@ -297,7 +306,7 @@ RLM_API void realm_dart_void_completion_callback(realm_userdata_t userdata, cons
297306
struct apikey_buf : realm_app_user_apikey
298307
{
299308
apikey_buf(const realm_app_user_apikey& apikey_input)
300-
: key_buffer(apikey_input.key ? apikey_input.key : ""),
309+
: key_buffer(apikey_input.key ? apikey_input.key : ""),
301310
name_buffer(apikey_input.name ? apikey_input.name : "")
302311
{
303312
id = apikey_input.id;
@@ -341,8 +350,8 @@ std::vector<apikey_buf> realm_apikey_list_copy(const realm_app_user_apikey_t api
341350
apikey_list + count,
342351
std::back_inserter(apikey_list_copy),
343352
[](const realm_app_user_apikey_t& apikey) {
344-
return apikey_buf(apikey);
345-
}
353+
return apikey_buf(apikey);
354+
}
346355
);
347356
}
348357
return apikey_list_copy;
@@ -362,24 +371,24 @@ RLM_API void realm_dart_apikey_list_callback(realm_userdata_t userdata, realm_ap
362371
apikey_list_buf.end(),
363372
std::back_inserter(apikey_list),
364373
[](const apikey_buf& apikey) {
365-
return realm_app_user_apikey{
366-
apikey.id,
367-
apikey.key_buffer.c_str(),
368-
apikey.name_buffer.c_str(),
369-
apikey.disabled
370-
};
371-
}
374+
return realm_app_user_apikey{
375+
apikey.id,
376+
apikey.key_buffer.c_str(),
377+
apikey.name_buffer.c_str(),
378+
apikey.disabled
379+
};
380+
}
372381
);
373382
(reinterpret_cast<realm_return_apikey_list_func_t>(ud->dart_callback))(ud->handle, apikey_list.data(), apikey_list.size(), error.get());
374383
});
375384
}
376385

377386
RLM_API void realm_dart_return_string_callback(realm_userdata_t userdata, const char* serialized_ejson_response, const realm_app_error_t* error) {
378387
auto error_copy = realm_app_error_copy(error);
379-
std::string buf{serialized_ejson_response ? serialized_ejson_response : ""};
388+
std::string buf{ serialized_ejson_response ? serialized_ejson_response : "" };
380389

381390
auto ud = reinterpret_cast<realm_dart_userdata_async_t>(userdata);
382391
ud->scheduler->invoke([ud, buf = std::move(buf), error = std::move(error_copy)]() mutable {
383392
(reinterpret_cast<realm_return_string_func_t>(ud->dart_callback))(ud->handle, buf.data(), error.get());
384393
});
385-
}
394+
}

src/realm_dart_sync.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ RLM_API void realm_dart_sync_connection_state_changed_callback(realm_userdata_t
3737
realm_sync_connection_state_e old_state,
3838
realm_sync_connection_state_e new_state);
3939

40+
RLM_API void realm_dart_user_change_callback(realm_userdata_t userdata, realm_user_state_e state);
41+
4042
RLM_API void realm_dart_sync_on_subscription_state_changed_callback(realm_userdata_t userdata, realm_flx_sync_subscription_set_state_e state);
4143

4244
RLM_API bool realm_dart_sync_before_reset_handler_callback(realm_userdata_t userdata, realm_t* realm);

test/user_test.dart

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
//
1717
////////////////////////////////////////////////////////////////////////////////
1818
19+
import 'dart:async';
1920
import 'dart:isolate';
20-
import 'dart:math';
2121

2222
import 'package:test/expect.dart' hide throws;
2323

@@ -489,4 +489,26 @@ void main() {
489489
.having((e) => e.statusCode, 'statusCode', 401)
490490
.having((e) => e.linkToServerLogs, 'linkToServerLogs', contains('logs?co_id='))));
491491
});
492+
493+
baasTest('User.logOut raises changes', (appConfig) async {
494+
final app = App(appConfig);
495+
final user = await getIntegrationUser(app);
496+
497+
expect(user.state, UserState.loggedIn);
498+
499+
final completer = Completer<UserChanges>();
500+
final subscription = user.changes.listen((event) {
501+
completer.complete(event);
502+
});
503+
504+
await user.logOut();
505+
506+
expect(user.state, UserState.loggedOut);
507+
508+
final changeEvent = await completer.future.timeout(Duration(seconds: 15));
509+
expect(changeEvent.user, user);
510+
expect(changeEvent.user.state, UserState.loggedOut);
511+
512+
await subscription.cancel();
513+
});
492514
}

0 commit comments

Comments
 (0)