Realm Platform offers the ability to register for data change events, also called "Event Handling." This functionality is provided in the server-side Node.js and .NET SDKs via a global event listener API which hooks into the Realm Object Server, allowing you to observe changes across Realms. This could mean listening to every Realm for changes, or Realms that match a specific pattern. For example, if your app architecture separated user settings data into a Realm unique for each user where the virtual path was /~/settings
, then a listener could be setup to react to changes to any user’s settings
Realm.
Whenever a change is synchronized to the server, it triggers a notification which allows you to run custom server-side logic in response to the change. The notification will inform you about the virtual path of the updated Realm and provide the Realm object and fine-grained information on which objects changed. The change set provides the object indexes broken down by class name for any inserted, deleted, or modified object in the last synchronized transaction.
Looking for a complete code sample? Skip ahead to our examples section.
To use Realm's event handling, you’ll need to create a small Node.js application.
Create a directory to place the server files, then create a file named package.json
. This JSON file is used by Node.js and npm, its package manager, to describe an application and specify external dependencies.
You can create this file interactively by using npm init
. You can also fill in a simple skeleton file yourself using your text editor:
{"name": "MyApp","version": "0.0.1","main": "index.js","author": "Your Name","description": "My Cool Realm App","dependencies":{"realm":"latest"}}
After the package.json
file is configured properly, type:
npm install
to download, unpack and configure all the modules and their dependencies.
Your event handler will need to access the Object Server with administrative privileges. In this example, we use the default realm-admin
and specify our credentials in our Realm.Sync.User.login
call.
This can be used to synchronously construct aRealm.Sync.User
object which can be passed into theRealm
constructor to open a connection to any Realm on the server side.
A sample index.js
file might look something like this. This example listens for changes to a user-specific private Realm at the virtual path /~/private
. It will look for updated Coupon
objects in these Realms, verify their coupon code if it wasn’t verified yet, and write the result of the verification into the isValid
property of the Coupon
object.
'use strict';​var Realm = require('realm');​// the URL to the Realm Object Servervar SERVER_URL = '//127.0.0.1:9080';​// The regular expression you provide restricts the observed Realm files to only the subset you// are actually interested in. This is done in a separate step to avoid the cost// of computing the fine-grained change set if it's not necessary.var NOTIFIER_PATH = '^/([^/]+)/private$';​//declare admin userlet adminUser = undefined​// The handleChange callback is called for every observed Realm file whenever it// has changes. It is called with a change event which contains the path, the Realm,// a version of the Realm from before the change, and indexes indication all objects// which were added, deleted, or modified in this changevar handleChange = async function (changeEvent) {// Extract the user ID from the virtual path, assuming that we're using// a filter which only subscribes us to updates of user-scoped Realms.var matches = changeEvent.path.match("^/([^/]+)/([^/]+)$");var userId = matches[1];​var realm = changeEvent.realm;var coupons = realm.objects('Coupon');var couponIndexes = changeEvent.changes.Coupon.insertions;​for (let couponIndex of couponIndexes) {var coupon = coupons[couponIndex];if (coupon.isValid !== undefined) {var isValid = verifyCouponForUser(coupon, userId);realm.write(function() {coupon.isValid = isValid;});}}}​function verifyCouponForUser(coupon, userId) {//logic for verifying a coupon's validity}​// register the event handler callbackasync function main() {adminUser = await Realm.Sync.User.login(`https:${SERVER_URL}`, 'realm-admin', '')Realm.Sync.addListener(`realms:${SERVER_URL}`, adminUser, NOTIFIER_PATH, 'change', handleChange);}​main()
The heart of the event handler is thehandleChange()
function, which is passed achangeEvent
object. This object has four keys:
path
: The path of the changed Realm (used above with match
to extract the user ID)
realm
: the changed Realm itself
oldRealm
: the changed Realm in its old state, before the changes were applied
changes
: an object containing a hash map of the Realm’s changed objects
The changes
object itself has a more complicated structure: it’s a series of key/value pairs, the keys of which are the names of objects (i.e., Coupon
in the above code), and the values of which are another object with key/value pairs listing insertions, deletions, and modifications to those objects. The values of those keys are index values into the Realm. Here’s the overall structure of thechangeEvent
object:
{path: "realms://server/user/realm",realm: <realm object>,oldRealm: <realm object>,changes: {objectType1: {insertions: [ a, b, c, ...],deletions: [ a, b, c, ...],modifications: [ a, b, c, ...]},objectType2: {insertions: [ a, b, c, ...],deletions: [ a, b, c, ...],modifications: [ a, b, c, ...]}}}
In the example above, we get the Coupons and the indexes of the newly inserted coupons with this:
var realm = changeEvent.realm;var coupons = realm.objects('Coupon');var couponIndexes = changeEvent.changes.Coupon.insertions;
Then, we use for (let couponIndex of couponIndexes)
to loop through the indexes and to get each changed coupon.
To use Realm Event Handling, you’ll need to create a .NET application. It can be a console app or Asp.NET app and can run on all flavours of Linux, macOS, or Windows that the .NET SDK supports.
Create a new project or open your existing one and add the Realm NuGet package.
You’ll need to create a class that implements the INotificationHandler interface. It has two methods - ShouldHandle and HandleChangesAsync.
To tell the notifier that your handler is interested in observing changes for a particular path, return true
in the ShouldHandle
callback. As a general principle, ShouldHandle
should always return stable result every time it is invoked with the same path and should return as quickly as possible to avoid blocking observing of notifications. We’ve provided an abstract implementation of the INotificationHandler
interface in a Regex​Notification​Handler class that implements ShouldHandle
in terms of regex matching the string that is passed to its constructor. For example, if you want to observe changes to the user-specific private Realm at virtual path /~/private
, you would provide the following implementation:
class PrivateHandler : RegexNotificationHandler{public PrivateHandler() : base("^/.*/private$"){}​public override async Task HandleChangeAsync(IChangeDetails details){// TODO: handle change notifications}}
The HandleChangesAsync
method is the heart of the event handler - it gets invoked whenever a Realm at an observed path (ShouldHandle
returned true
) changes and is passed an IChangeDetails object that contains detailed information about the change that occurred. It has the following properties and methods:
 PreviousRealm and CurrentRealm are readonly snapshots of the state of the Realm just before and just after the change has occurred. PreviousRealm
may be null if the notification was received just after creating the Realm. CurrentRealm
can never be null.
 RealmPath is the virtual path of the Realm that has changed, e.g. /some-user-id/private
.
 GetRealmForWriting() can be invoked to get a writeable instance of the Realm that has changed in case you need to write some data in response to the change notification. Since writing to any Realm automatically advances it to the latest version, this Realm instance may contain slightly newer data than CurrentRealm
if new changes have been received while handling the notification. Unlike CurrentRealm
and PreviousRealm
, the lifetime of this instance is not managed by the notifier, so make sure to place it inside a using
statement or manually dispose it as soon as you’re done with it.
 Changes is a dictionary of class names and IChangeSetDetails objects. For each object type that has changed, you’ll get a corresponding IChangeSetDetails
object containing Insertions, Deletions, and Modifications collections. Each of those contains IModification​Details instances with the following properties:
 PreviousIndex
indicates the index of the changed object in the PreviousRealm
view. It will be -1
if the object was inserted.
 PreviousObject
is the state of the object just before it has changed. It will be null
if the object was inserted.
 CurrentIndex
indicates the index of the changed object in the CurrentRealm
view. It will be -1
if the object was deleted.
 CurrentObject
is the state of the object just after it has changed. It will be null
if the object was deleted.
Here’s what a sample event handling application might look like in .NET:
public class Program{private const string AdminUsername = "admin@foo.com";private const string AdminPassword = "super-secure-password";private const string ServerUrl = "127.0.0.1:9080";​public static void Main(string[] args) => MainAsync().Wait();​public static async Task MainAsync(){​// Login the admin uservar credentials = Credentials.UsernamePassword(AdminUsername, AdminPassword, createUser: false);var admin = await User.LoginAsync(credentials, new Uri($"http://{ServerUrl}"));​var config = new NotifierConfiguration(admin){// Add all handlers that this notifier will invokeHandlers = { new CouponHandler() }};​// Start the notifier. Your handlers will be invoked for as// long as the notifier is not disposed.using (var notifier = await Notifier.StartAsync(config)){do{Console.WriteLine("Type in 'exit' to quit the app.");}while (Console.ReadLine() != "exit");}}​class CouponHandler : RegexNotificationHandler{// The regular expression you provide restricts the observed Realm files// to only the subset you are actually interested in. This is done to// avoid the cost of computing the fine-grained change set if it's not// necessary.public CouponHandler() : base($"^/.*/private$"){}​// The HandleChangeAsync method is called for every observed Realm file// whenever it has changes. It is called with a change event which contains// a version of the Realm from before and after the change, as well as// collections of all objects which were added, deleted, or modified in this changepublic override async Task HandleChangeAsync(IChangeDetails details){if (details.TryGetValue("Coupon", out var changeSetDetails) &&changeSetDetails.Insertions.Length > 0){// Extract the user ID from the virtual path, assuming that we're using// a filter which only subscribes us to updates of user-scoped Realms.var userId = details.RealmPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)[0];​using (var realm = details.GetRealmForWriting()){foreach (var coupon in changeSetDetails.Insertions.Select(c => c.CurrentObject)){var isValid = await CouponVerifier.VerifyAsync(coupon, userId);​// Create a ThreadSafeReference of the coupon. While both// details.CurrentRealm and details.GetRealmForWriting() are open// on the same thread, they are at different versions, so you need// to pass the Coupon between them either via ThreadSafeReference// or by its PrimaryKey.var writeableCoupon = realm.ResolveReference(ThreadSafeReference.Create(coupon));​// It may be null if the coupon was deleted by the time we get hereif (writeableCoupon != null){realm.Write(() => writeableCoupon.IsValid = isValid);}}}}}}}
Notes
Multiple Notifiers may be started (via Notifier.StartAsync) but they need to have a different WorkingDirectory specified to avoid errors.
HandleChangesAsync
will be invoked in parallel for different Realms but sequentially for changes on a single Realm. This means that if your code takes a lot of time to return from the function, a queue of notifications may build up for that particular Realm. Make sure to design your architecture with that in mind.
Asynchronous calls inside HandleChangesAsync
must not use ConfigureAwait(false)
as that will dispatch the continuation on a different thread making all Realm and RealmObject instances (that were open prior to the async operation) inaccessible from that thread.
The event handler callback provides access to detailed change information through a passed in change event object. This includes the indexes for the objects corresponding to:
Insertions
Modifications
Deletions
The change information only applies at an object-level. If you need property-level change information, an additional data adapter API is available in Javascript which is designed to pass every database operation. It forms the basis for our pre-built database connectors. Read more here:
To access the inserted or modified objects, you can access the Realm
object included in the change event object:
var handleChange = async function (changeEvent) {// Get the current Realmvar realm = changeEvent.realm;// Retrieve all objects of the relevant typevar coupons = realm.objects('Coupon');// Retrieve the indexes for the insertions/modificationsvar couponIndexes = changeEvent.changes.Coupon.insertions;​for (let couponIndex of couponIndexes) {// Use the Results object to retrieve the inserted/modified objectvar coupon = coupons[couponIndex];​//..}}
// The HandleChangeAsync method is called for every observed Realm file// whenever it has changes. It is called with a change event which contains// a version of the Realm from before and after the change, as well as// collections of all objects which were added, deleted, or modified in this changepublic override async Task HandleChangeAsync(IChangeDetails details){// Retrieve the indexes for the insertions/modificationsif (details.TryGetValue("Coupon", out var changeSetDetails) &&changeSetDetails.Insertions.Length > 0){// Get the current Realm// If you want a read-only version, use .CurrentRealm propertyusing (var realm = details.GetRealmForWriting()){// Use the Results object to retrieve the inserted/modified objectforeach (var coupon in changeSetDetails.Insertions.Select(c => c.CurrentObject)){//..}}}}
To access the deleted objects, you cannot use the Realm
object included in the change event object. The reason is that the Realm
object is at the current state of the database after the changes have been applied. This means the deleted objects are already removed. However, because Realm has an MVCC architecture, it is possible to provide a second view of the database that is at the state before the change.
This previous or old state is provided in the oldRealm
object included in the change event object. Use this to retrieve the delete objects:
var handleChange = async function (changeEvent) {// Get the old Realm that is at the state before the changevar oldRealm = changeEvent.oldRealm;// Retrieve all objects of the relevant typevar coupons = oldRealm.objects('Coupon');// Retrieve the indexes for the deletionsvar couponIndexes = changeEvent.changes.Coupon.deletions;​for (let couponIndex of couponIndexes) {// Use the Results object to retrieve the deleted objectvar deletedCoupone = coupons[couponIndex];​//..}}
// The HandleChangeAsync method is called for every observed Realm file// whenever it has changes. It is called with a change event which contains// a version of the Realm from before and after the change, as well as// collections of all objects which were added, deleted, or modified in this changepublic override async Task HandleChangeAsync(IChangeDetails details){// Retrieve the indexes for the insertions/modificationsif (details.TryGetValue("Coupon", out var changeSetDetails) &&changeSetDetails.Deletions.Length > 0){// Get the previous Realm// This Realm is read-only!using (var realm = details.PreviousRealm){// Use the Results object to retrieve the deleted objectforeach (var coupon in changeSetDetails.Deletions.Select(c => c.CurrentObject)){//..}}}}
A great potential use case for our event handler is integration with a 3rd party API. When a change is made to the Realm Object Server, a call can be made to a 3rd party API and then the results can easily be written to a synchronized Realm.
The following example was made to be easily used with our ToDo app tutorial. It takes advantage of the wit.ai API which can be used for basic text recognition.
Before running the event handler, install the dependencies via NPM
npm install realmnpm install node-wit
If you choose to use this same API, you'll need to sign up to get an API token, and you'll need to configure a wit/datetime
entity.
You'll need to fill out a number of constants within the script:
Wit Access Token
Server URL
Admin Login Credentials (this assumes the default realm-admin user and password)
The script assumes communication over https
'use strict';​var fs = require('fs');var Realm = require('realm');const { Wit, log } = require('node-wit');​// Server access token from wit.ai APIvar WIT_ACCESS_TOKEN = "INSERT_YOUR_WIT_ACCESS_TOKEN";​// The URL to the Realm Object Server//format should be: 'IP_ADDRESS:port' like example below// var SERVER_URL = '127.0.0.1:9080';var SERVER_URL = 'INSERT_SERVER_URL';​// The path used by the global notifier to listen for changes across all// Realms that match.var NOTIFIER_PATH = "/ToDo";​const client = new Wit({ accessToken: WIT_ACCESS_TOKEN })​//enable debugging if needed//Realm.Sync.setLogLevel('debug');​let adminUser = undefined//INSERT HANDLER HEREvar handleChange = async function(changeEvent) {const realm = changeEvent.realm;const tasks = realm.objects('Item');console.log(tasks);​console.log('received change event');​const taskInsertIndexes = changeEvent.changes.Item.insertions;const taskModIndexes = changeEvent.changes.Item.modifications;const taskDeleteIndexes = changeEvent.changes.Item.deletions;​for (var i = 0; i < taskInsertIndexes.length; i++) {const task = tasks[taskInsertIndexes[i]];console.log(task);let itemId = task.itemIdif (task !== undefined) {console.log('insertion occurred');const client = new Wit({ accessToken: WIT_ACCESS_TOKEN });const data = await client.message(task.body, {})console.log("Response received from wit: " + JSON.stringify(data));if (data.entities.datetime) {var dateTime = data.entities.datetime[0];if (!dateTime) {console.log("Couldn't find a date.");return;}realm.write(() => {task.body = `${task.body} - Date: ${dateTime.value}`})}}}}async function main() {adminUser = await Realm.Sync.User.login(`https://${SERVER_URL}`, 'realm-admin', 'password')Realm.Sync.addListener(`realms://${SERVER_URL}`, adminUser, NOTIFIER_PATH, 'change', handleChange);console.log('listening');}​main()
After filling out the constants, you can run the handler with node like:
node eventHandler.js
The following examples can be used for getting started quickly or as references for how to write a backend integration using the Realm event handler. The examples are mostly written using the Node.js SDK.
Not what you were looking for? Leave Feedback​