Skip to content

Commit

Permalink
Implement User/App listener (#4522)
Browse files Browse the repository at this point in the history
* Add change listener for app and user

* Add a member variable to store listener tokens to app and user.
Store the shared pointer to app and user as a member variable on
User and App classes to ensure that the tokens are moved correctly
and not copied.

* Write tests for app listener.

* Update changelog

* Add typescript types

* Add jsdocs

* Adding stubs for App and User listeners

* Update description of listener events

* Add Remove All Listeners

* Add removeAllListeners to realm-web

* Use NotificationBucket for App & User (#4551)

* Refactored NotificationBucket to take a Token type

* Using NotificationBucket for App and User

* Add header comment blocks to new functions

* Small refactorings

Co-authored-by: Kræn Hansen <[email protected]>
Co-authored-by: Tom Duncalf <[email protected]>
Co-authored-by: Kenneth Geisshirt <[email protected]>
Co-authored-by: FFranck <[email protected]>
  • Loading branch information
5 people authored May 4, 2022
1 parent 6140d46 commit b4d6033
Show file tree
Hide file tree
Showing 17 changed files with 428 additions and 84 deletions.
8 changes: 5 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,29 +42,31 @@
"type": "node",
"request": "launch",
"name": "Debug Node Unit Tests (rebuild)",
"preLaunchTask": "rebuild-node-tests",
"preLaunchTask": "Build Node Tests",
"cwd": "${workspaceRoot}/tests",
"console": "integratedTerminal",
"program": "${workspaceRoot}/tests/node_modules/jasmine/bin/jasmine.js",
"runtimeArgs": [
"--expose_gc"
],
"args": [
"spec/unit_tests.js",
"--filter=."
"--filter=${input:testFilter}"
]
},
{
"type": "node",
"request": "launch",
"name": "Debug Node Unit Tests",
"cwd": "${workspaceRoot}/tests",
"console": "integratedTerminal",
"program": "${workspaceRoot}/tests/node_modules/jasmine/bin/jasmine.js",
"runtimeArgs": [
"--expose_gc"
],
"args": [
"spec/unit_tests.js",
"--filter=."
"--filter=${input:testFilter}"
]
},
{
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
x.x.x Release notes (yyyy-MM-dd)
=============================================================
### Enhancements
* None.
* Add ability to listen to changes to `Realm.App` and `Realm.User`. ([#4455](https://github.com/realm/realm-js/issues/4455))

### Fixed
* Fixed issue where React Native apps would sometimes show stale Realm data until the user interacted with the app UI ([#4389](https://github.com/realm/realm-js/issues/4389), since v10.0.0)
Expand Down
36 changes: 36 additions & 0 deletions docs/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,24 @@ class App {
* @since v10.0.0
*/
getApp(appId) {}

/**
* Adds a listener that will be fired on various user events.
* This includes login, logout, switching users, linking users and refreshing custom data.
* @param {function} callback
*/
addListener(callback) {}

/**
* Removes an event listener (see {@link addListener})
* @param {function} callback
*/
removeListener(callback) {}

/**
* Removes all event listeners
*/
removeListener() {}
}

/**
Expand Down Expand Up @@ -876,6 +894,24 @@ class User {
* @returns {Realm.User~Push}
*/
push(serviceName) {}

/**
* Adds a listener that will be fired on various user related events.
* This includes auth token refresh, refresh token refresh, refresh custom user data, and logout.
* @param {function} callback
*/
addListener(callback) {}

/**
* Removes an event listener (see {@link addListener})
* @param {function} callback
*/
removeListener(callback) {}

/**
* Removes all event listeners
*/
removeAllListeners() {}
}

/**
Expand Down
21 changes: 21 additions & 0 deletions packages/realm-web/src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,27 @@ export class App<
await this.removeUser(user);
}

/**
* @inheritdoc
*/
public addListener(): void {
throw new Error("Not yet implemented");
}

/**
* @inheritdoc
*/
public removeListener(): void {
throw new Error("Not yet implemented");
}

/**
* @inheritdoc
*/
public removeAllListeners(): void {
throw new Error("Not yet implemented");
}

/**
* The currently active user (or null if no active users exists).
*
Expand Down
21 changes: 21 additions & 0 deletions packages/realm-web/src/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,27 @@ export class User<
return this.customData;
}

/**
* @inheritdoc
*/
public addListener(): void {
throw new Error("Not yet implemented");
}

/**
* @inheritdoc
*/
public removeListener(): void {
throw new Error("Not yet implemented");
}

/**
* @inheritdoc
*/
public removeAllListeners(): void {
throw new Error("Not yet implemented");
}

/** @inheritdoc */
public callFunction<ReturnType = unknown>(name: string, ...args: unknown[]): Promise<ReturnType> {
return this.functions.callFunction(name, ...args);
Expand Down
118 changes: 104 additions & 14 deletions src/js_app.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,39 @@

#include <realm/object-store/util/event_loop_dispatcher.hpp>

#include "js_notifications.hpp"
#include "js_user.hpp"
#include "js_app_credentials.hpp"
#include "js_network_transport.hpp"
#include "js_email_password_auth.hpp"
#include "realm/object-store/sync/subscribable.hpp"

using SharedApp = std::shared_ptr<realm::app::App>;
using SharedUser = std::shared_ptr<realm::SyncUser>;
using AppToken = realm::Subscribable<realm::app::App>::Token;

namespace realm {
namespace js {

template <typename T>
class AppClass : public ClassDefinition<T, SharedApp> {
class App {
public:
App(const SharedApp& l)
: m_app(l)
{
}

// Remove copy constructors to avoid destroying the listener Token
App(const App&) = delete;
App& operator=(const App&) = delete;

notifications::NotificationHandle<T, AppToken> m_notification_handle;

SharedApp m_app;
};

template <typename T>
class AppClass : public ClassDefinition<T, realm::js::App<T>> {
using ContextType = typename T::Context;
using FunctionType = typename T::Function;
using ObjectType = typename T::Object;
Expand All @@ -49,12 +69,14 @@ class AppClass : public ClassDefinition<T, SharedApp> {
using Function = js::Function<T>;
using ReturnValue = js::ReturnValue<T>;
using Arguments = js::Arguments<T>;
using NotificationBucket = notifications::NotificationBucket<T, AppToken>;
using NetworkTransport = JavaScriptNetworkTransport<T>;
using NetworkTransportFactory = typename NetworkTransport::NetworkTransportFactory;

public:
const std::string name = "App";


/**
* Generates instances of GenericNetworkTransport, eventually allowing Realm Object Store to perform network
* requests. Exposed to allow other components (ex the RPCServer) to override the underlying implementation.
Expand Down Expand Up @@ -99,12 +121,19 @@ class AppClass : public ClassDefinition<T, SharedApp> {
static void clear_app_cache(ContextType, ObjectType, Arguments&, ReturnValue&);
static void get_app(ContextType, ObjectType, Arguments&, ReturnValue&);
static void set_versions(ContextType, ObjectType, Arguments&, ReturnValue&);
static void add_listener(ContextType, ObjectType, Arguments&, ReturnValue&);
static void remove_listener(ContextType, ObjectType, Arguments&, ReturnValue&);
static void remove_all_listeners(ContextType, ObjectType, Arguments&, ReturnValue&);


MethodMap<T> const methods = {
{"_logIn", wrap<log_in>},
{"switchUser", wrap<switch_user>},
{"_removeUser", wrap<remove_user>},
{"_deleteUser", wrap<delete_user>},
{"addListener", wrap<add_listener>},
{"removeListener", wrap<remove_listener>},
{"removeAllListeners", wrap<remove_all_listeners>},
};

MethodMap<T> const static_methods = {
Expand All @@ -120,7 +149,7 @@ inline typename T::Function AppClass<T>::create_constructor(ContextType ctx)
template <typename T>
inline typename T::Object AppClass<T>::create_instance(ContextType ctx, SharedApp app)
{
return create_object<T, AppClass<T>>(ctx, new SharedApp(app));
return create_object<T, AppClass<T>>(ctx, new realm::js::App<T>(app));
}

template <typename T>
Expand Down Expand Up @@ -204,7 +233,7 @@ void AppClass<T>::constructor(ContextType ctx, ObjectType this_object, Arguments

SharedApp app = app::App::get_shared_app(config, client_config);

set_internal<T, AppClass<T>>(ctx, this_object, new SharedApp(app));
set_internal<T, AppClass<T>>(ctx, this_object, new realm::js::App<T>(app));
}

template <typename T>
Expand All @@ -217,7 +246,7 @@ std::string AppClass<T>::get_user_agent()
template <typename T>
void AppClass<T>::get_app_id(ContextType ctx, ObjectType this_object, ReturnValue& return_value)
{
auto app = *get_internal<T, AppClass<T>>(ctx, this_object);
auto app = get_internal<T, AppClass<T>>(ctx, this_object)->m_app;
return_value.set(Value::from_string(ctx, app->config().app_id));
}

Expand All @@ -226,7 +255,7 @@ void AppClass<T>::log_in(ContextType ctx, ObjectType this_object, Arguments& arg
{
args.validate_maximum(2);

auto app = *get_internal<T, AppClass<T>>(ctx, this_object);
auto app = get_internal<T, AppClass<T>>(ctx, this_object)->m_app;

auto credentials_object = Value::validated_to_object(ctx, args[0]);
auto callback_function = Value::validated_to_function(ctx, args[1]);
Expand All @@ -244,7 +273,7 @@ void AppClass<T>::log_in(ContextType ctx, ObjectType this_object, Arguments& arg
template <typename T>
void AppClass<T>::get_all_users(ContextType ctx, ObjectType this_object, ReturnValue& return_value)
{
auto app = *get_internal<T, AppClass<T>>(ctx, this_object);
auto app = get_internal<T, AppClass<T>>(ctx, this_object)->m_app;

auto users = Object::create_empty(ctx);
for (auto user : app->all_users()) {
Expand All @@ -259,7 +288,7 @@ void AppClass<T>::get_all_users(ContextType ctx, ObjectType this_object, ReturnV
template <typename T>
void AppClass<T>::get_current_user(ContextType ctx, ObjectType this_object, ReturnValue& return_value)
{
auto app = *get_internal<T, AppClass<T>>(ctx, this_object);
auto app = get_internal<T, AppClass<T>>(ctx, this_object)->m_app;
auto user = app->current_user();
if (user) {
return_value.set(create_object<T, UserClass<T>>(ctx, new User<T>(std::move(user), std::move(app))));
Expand All @@ -274,10 +303,10 @@ void AppClass<T>::switch_user(ContextType ctx, ObjectType this_object, Arguments
{
args.validate_count(1);

auto app = *get_internal<T, AppClass<T>>(ctx, this_object);
auto app = get_internal<T, AppClass<T>>(ctx, this_object)->m_app;
auto user = get_internal<T, UserClass<T>>(ctx, Value::validated_to_object(ctx, args[0], "user"));

app->switch_user(*user);
app->switch_user(user->m_user);
return_value.set(Value::from_undefined(ctx));
}

Expand All @@ -286,11 +315,11 @@ void AppClass<T>::remove_user(ContextType ctx, ObjectType this_object, Arguments
{
args.validate_count(2);

auto app = *get_internal<T, AppClass<T>>(ctx, this_object);
auto app = get_internal<T, AppClass<T>>(ctx, this_object)->m_app;
auto user = get_internal<T, UserClass<T>>(ctx, Value::validated_to_object(ctx, args[0], "user"));
auto callback = Value::validated_to_function(ctx, args[1], "callback");

app->remove_user(*user, Function::wrap_void_callback(ctx, this_object, callback));
app->remove_user(user->m_user, Function::wrap_void_callback(ctx, this_object, callback));
}

/**
Expand All @@ -309,18 +338,18 @@ void AppClass<T>::delete_user(ContextType ctx, ObjectType this_object, Arguments
{
args.validate_count(2);

auto app = *get_internal<T, AppClass<T>>(ctx, this_object);
auto app = get_internal<T, AppClass<T>>(ctx, this_object)->m_app;
auto user = get_internal<T, UserClass<T>>(ctx, Value::validated_to_object(ctx, args[0], "user"));
auto callback = Value::validated_to_function(ctx, args[1], "callback");

app->delete_user(*user, Function::wrap_void_callback(ctx, this_object, callback));
app->delete_user(user->m_user, Function::wrap_void_callback(ctx, this_object, callback));
}


template <typename T>
void AppClass<T>::get_email_password_auth(ContextType ctx, ObjectType this_object, ReturnValue& return_value)
{
auto app = *get_internal<T, AppClass<T>>(ctx, this_object);
auto app = get_internal<T, AppClass<T>>(ctx, this_object)->m_app;
return_value.set(EmailPasswordAuthClass<T>::create_instance(ctx, app));
}

Expand Down Expand Up @@ -354,6 +383,67 @@ void AppClass<T>::set_versions(ContextType ctx, ObjectType this_object, Argument
AppClass<T>::platform_os = Object::validated_get_string(ctx, versions, "platformOs");
AppClass<T>::platform_version = Object::validated_get_string(ctx, versions, "platformVersion");
}
/**
* @brief Registers an event listener on the SharedApp that fires on various app events.
* This includes login, logout, switching users, linking users and refreshing custom data.
*
* @param ctx JS context
* @param this_object JS's object holding the `AppClass`
* @param args Contains a callback that will be called on an event
* @param return_value \ref void
*/
template <typename T>
void AppClass<T>::add_listener(ContextType ctx, ObjectType this_object, Arguments& args, ReturnValue& return_value)
{
args.validate_count(1);
auto callback = Value::validated_to_function(ctx, args[0], "callback");
auto app = get_internal<T, AppClass<T>>(ctx, this_object);
Protected<FunctionType> protected_callback(ctx, callback);
Protected<ObjectType> protected_this(ctx, this_object);
Protected<typename T::GlobalContext> protected_ctx(Context::get_global_context(ctx));

auto token = std::move(app->m_app->subscribe([=](const realm::app::App&) {
Function::callback(protected_ctx, protected_callback, 0, {});
}));

NotificationBucket::emplace(app->m_notification_handle, std::move(protected_callback), std::move(token));
}

/**
* @brief Removes the event listener for the provided callback.
*
* @param ctx JS context
* @param this_object JS's object holding the `AppClass`
* @param args Contains a callback function that was given to `addListener`
* @param return_value \ref void
*/
template <typename T>
void AppClass<T>::remove_listener(ContextType ctx, ObjectType this_object, Arguments& args, ReturnValue& return_value)
{
args.validate_count(1);
auto callback = Value::validated_to_function(ctx, args[0], "callback");
auto app = get_internal<T, AppClass<T>>(ctx, this_object);
Protected<FunctionType> protected_callback(ctx, callback);

NotificationBucket::erase(app->m_notification_handle, std::move(protected_callback));
}

/**
* @brief Removes all registered event listeners.
*
* @param ctx JS context
* @param this_object JS's object holding the `AppClass`
* @param args No arguments
* @param return_value \ref void
*/
template <typename T>
void AppClass<T>::remove_all_listeners(ContextType ctx, ObjectType this_object, Arguments& args,
ReturnValue& return_value)
{
args.validate_count(0);
auto app = get_internal<T, AppClass<T>>(ctx, this_object);
NotificationBucket::erase(app->m_notification_handle);
}

} // namespace js
} // namespace realm
Loading

0 comments on commit b4d6033

Please sign in to comment.