Manage Sync Subscriptions - Node.js SDK
On this page
- Prerequisites
- Subscribe to Queries
- Subscribe to a Query
- Subscribe to a Query without a Subscription Name
- Wait for a Query Subscription to Sync
- Unsubscribe from a Query
- Manually Manage Subscriptions
- Get All Subscriptions
- Add a Subscription
- Set Initial Subscriptions
- Check the Status of Subscriptions
- Subscription State "Complete"
- Update Subscriptions with a New Query
- Remove Subscriptions
- Remove a Subscription by Query
- Remove a Subscription by Name
- Remove a Subscription by Reference
- Remove All Subscriptions to an Object Type
- Remove All Unnamed Subscriptions
- Remove All Subscriptions
- Performance Considerations
- API Efficiency
- Group Updates for Improved Performance
- Flexible Sync RQL Requirements and Limitations
- Indexed Queryable Fields Subscription Requirements
- Unsupported Query Operators in Flexible Sync
- List Queries
- Embedded or Linked Objects
Flexible Sync uses subscriptions and permissions to determine what data to sync with your App. You must have at least one subscription before you can read from or write to a realm with Flexible Sync enabled. This page details how to manage those subscriptions.
You can add, update, and remove query subscriptions to control what data syncs to the client device. In the Realm Node.js SDK v12.0.0 and later, you can subscribe to queries instead of or in addition to manually managing subscriptions.
You can't create subscriptions for Data Ingest and asymmetric objects because they only send data to your app's backend.
Important
Flexible Sync Query Limitations
Flexible Sync subscriptions only support a subset of the RQL query operators. Refer to the Flexible Sync RQL Limitations documentation for information on which operators are not supported.
Prerequisites
You need to meet the following requirements before you can use Atlas Device Sync with the Node.js SDK:
A non-sharded Atlas cluster running MongoDB 5.0 or later.
Realm JavaScript version 10.12.0 or later.
In addition to the requirements, you need to set up the following to use Flexible Sync in a Node.js client:
Subscribe to Queries
New in version 12.0.0.
Realm Node.js v12.0.0 adds experimental APIs that subscribe to and unsubscribe from a query's results. These APIs abstract away the details of manually adding and removing subscriptions.
For all subscriptions, you need an authenticated user and a Flexible Sync realm.
Changed in version 12.3.0: Geospatial data supported in Atlas Device Sync
In the Realm Node.js SDK v12.3.0 and later, you can create subscriptions to geospatial queries. If you try to subscribe to a geospatial query with an older version of the SDK, you will receive a server error with a compensating write.
For more information, refer to Query Geospatial Data.
Subscribe to a Query
We recommend that you name your subscriptions. This makes finding and managing your subscriptions easier. Subscription names must be unique. Trying to add a subscription with the same name as an existing subscription throws an error.
To subscribe to a query:
Query for the objects that you want to read and write.
Call
subscribe()
on the query results to create a sync subscription for objects matching the query.Pass a
SubscriptionOptions
object that contains thename
property tosubscribe()
.
Subscribe to a Query without a Subscription Name
Most of the time, you should give your subscriptions a name. If you don't, the name is set to null.
If you use filtered()
on an unnamed query subscription, the subscription
identifier is based on the filtered
query. This means that every time your
query string changes, subscribe()
will create a new subscription.
Wait for a Query Subscription to Sync
When you subscribe to a query's results, the results do not contain objects
until synced data is downloaded. When you do need to wait for syned objects to
finish downloading, use waitForSync
. You can specify different behavior for
your subscriptions and how they handle witing for downloads.
This example uses the FirstTime
option, which is the default behavior.
A subscription with FirstTime
behavior only waits for sync to finish when a
subscription is first created.
import { WaitForSync } from "realm"; // Get tasks that have a status of "in progress". const completedTasks = realm .objects(Task) .filtered("status == 'completed'"); // Only waits for sync to finish on the initial sync. await completedTasks.subscribe({ behavior: WaitForSync.FirstTime, name: "First time sync only", });
The other supported WaitForSync
options are:
Always
: Wait to download matching objects every time your app launches. The app must have an internet connection at every launch.Never
: Never wait to download matching objects. The app needs an internet connection for the user to authenticate the first time the app launches, but can open offline on subsequent launches using cached credentials.
You can optionally specify a timeout
value to limit how long the sync download runs:
import { WaitForSync } from "realm"; // Get tasks that have a status of "in progress". const completedTasks = realm .objects(Task) .filtered("status == 'completed'"); // Add subscription with timeout // If timeout expires before sync is completed, currently-downloaded // objects are returned and sync download continues in the background. const taskSubscription = await completedTasks.subscribe({ behavior: WaitForSync.Always, timeout: 500, });
Unsubscribe from a Query
You can unsubscribe from a query's results using unsubscribe()
:
import { WaitForSync } from "realm"; // Get tasks that have a status of "in progress". const completedTasks = realm .objects(Task) .filtered("status == 'completed'"); // Only waits for sync to finish on the initial sync. await completedTasks.subscribe({ behavior: WaitForSync.FirstTime, name: "First time sync only", }); // Unsubscribe completedTasks.unsubscribe();
This removes the subscription from the list of active subscriptions, similar to manually removing a subscription.
A results list may still contain objects after calling unsubscribe()
if another subscription exists that contains overlapping objects.
When you call unsubscribe()
, the associated subscription is removed.
Subscriptions are removed by name. If they don't have a name, unsubscribe()
removes any queries that exactly match the one you call unsubscribe()
on.
The unsubscribe()
method returns before objects matching the removed
subscription are deleted from the realm. Sync continues in the background based
on the new set of subscriptions.
Manually Manage Subscriptions
You can use the Subscriptions API to manually manage a set of subscriptions to specific queries on queryable fields.
You can:
Get a list of all subscriptions
Add subscriptions
Check subscription state
Update subscriptions with new queries
Remove individual subscriptions or all subscriptions of a type
When data matches the subscription and has appropriate permissions, it syncs between devices and the backend application.
When you create a subscription, Realm looks for data matching a query on a specific object type. You can have subscriptions on several different object types. You can also have multiple queries on the same object type.
Important
Object Links
You must add both an object and its linked object to the subscription set to see a linked object.
If your subscription results contain an object with a property that links to an object not contained in the results, the link appears to be null. There is no way to distinguish whether that property's value is legitimately null, or whether the object it links to exists but is out of view of the query subscription.
Get All Subscriptions
When using a flexible synced realm, you can access a SubscriptionSet
, a
collection of subscriptions, through the realm.subscriptions property.
// get the SubscriptionSet for the realm const subscriptions = realm.subscriptions;
Add a Subscription
Subscriptions are based on the results of realm queries.
In the following example, completed
and progressMinutes
have been set
as queryable fields in an App Services App. In the client code, we create
filtered queries and then subscribe to their results:
Completed tasks
Completed tasks that have taken over 120
progressMinutes
const tasks = realm.objects("Task"); const longRunningTasks = tasks.filtered( 'status == "completed" && progressMinutes > 120' ); await realm.subscriptions.update((mutableSubs) => { mutableSubs.add(longRunningTasks, { name: "longRunningTasksSubscription", }); mutableSubs.add(realm.objects("Team"), { name: "teamsSubscription", }); });
Set Initial Subscriptions
You must have at least one subscription before you can read from or write to a Flexible Sync realm. You can add an initial subscription when opening a realm.
To set initial subscriptions, include the initialSubscriptions
field in
your realm's SyncConfiguration.
Within the initialSubscriptions
object, add an update
field set to a callback that subscribes to queries:
By default, initial subscriptions are only created the first time a realm is
opened. If your app needs to rerun this initial subscription every time the app
starts, you can set rerunOnOpen
to true
. You might need to do this to
re-run dynamic time ranges or other queries that require a re-computation of
static variables for the subscription.
Check the Status of Subscriptions
You can check the subscription state to see if the server has acknowledged the subscription and the device has downloaded the data locally.
You can use subscription state to:
Trigger error handling
Show if the transaction is pending or has completed
Find out when a subscription set is superseded, and you should obtain a new instance of the subscription set to write a subscription change
Note
Subscription State "Complete"
The subscription set state "complete" does not mean "sync is done" or "all documents have been synced". "Complete" means the following two things have happened:
The subscription has become the active subscription set that is currently being synchronized with the server.
The documents that matched the subscription at the time the subscription was sent to the server are now on the local device. Note that this does not necessarily include all documents that currently match the subscription.
The Realm SDK does not provide a way to check whether all documents that match a subscription have synced to the device.
New in version 12.0.0.
Node.js v12.0.0 added the SubscriptionSetState enum that you can use to get the status of a subscription.
Subscription State "Complete"
The subscription set state "complete" does not mean "sync is done" or "all documents have been synced". "Complete" means the following two things have happened:
The subscription has become the active subscription set that is currently being synchronized with the server.
The documents that matched the subscription at the time the subscription was sent to the server are now on the local device. Note that this does not necessarily include all documents that currently match the subscription.
The Realm SDK does not provide a way to check whether all documents that match a subscription have synced to the device.
Update Subscriptions with a New Query
You can update a named subscription with a new query. To update a subscription's
query, pass the new query and a subscription option with the name of the
subscription that you want to update to the MutableSubscriptionSet.add()
method. Like adding a new subscription, you must update a subscription within a
transaction by calling subscriptions.update()
.
In the following example, long-running tasks are re-defined to be any tasks that have taken more than 180 minutes.
realm.subscriptions.update((mutableSubs) => { mutableSubs.add( tasks.filtered('status == "completed" && progressMinutes > 180'), { name: "longRunningTasksSubscription", } ); });
Note
Attempting to update a subscription that has the
SubscriptionOptions.throwOnUpdate
field set to true throws an exception.
Remove Subscriptions
You can remove subscriptions in several ways:
Remove a single subscription with a specific query
Remove a single subscription with a specific name
Remove all subscriptions to a specific object model
Remove all unnamed subscriptions
Remove all subscriptions
When you remove a subscription query, the server also removes synced data from the client device.
Remove a Subscription by Query
You can remove a specific subscription by query by executing a transaction on
the subscriptions set. Pass the query to the remove() method on the
MutableSubscriptionSet
within a transaction.
In the following example, the subscription to tasks with an owner named 'Ben' is removed from the subscriptions set.
realm.subscriptions.update((mutableSubs) => { // remove a subscription with a specific query mutableSubs.remove(tasks.filtered('owner == "Ben"')); });
Remove a Subscription by Name
To remove a specific subscription by name, execute a transaction on the
subscriptions set. Within the transaction, pass the name to the
removeByName() method on the
MutableSubscriptionSet
.
realm.subscriptions.update((mutableSubs) => { // remove a subscription with a specific name mutableSubs.removeByName("longRunningTasksSubscription"); });
Remove a Subscription by Reference
If you have a reference to a subscription, you can remove that subscription. To
do so, execute a transaction on the subscriptions set. Within the transaction,
pass the reference variable to the removeSubscription method on the
MutableSubscriptionSet
.
let subscriptionReference; realm.subscriptions.update((mutableSubs) => { subscriptionReference = mutableSubs.add(realm.objects("Task")); }); // later.. realm.subscriptions.removeSubscription(subscriptionReference);
Remove All Subscriptions to an Object Type
To remove all subscriptions on a specific object type, execute a transaction on
the subscriptions set. Within the transaction, pass the object type as a string
to the removeByObjectType method on the
MutableSubscriptionSet
.
realm.subscriptions.update((mutableSubs) => { mutableSubs.removeByObjectType("Team"); });
Remove All Unnamed Subscriptions
New in version v12.0.0.
You may want to remove unnamed subscriptions that are transient or dynamically generated, but leave named subscriptions in place.
You can remove all unnamed subscriptions from the subscription set by
calling .removeUnnamed()
on mutableSubs
. .removeUnnamed()
returns
the number of unnamed subscriptions removed.
// Remove unnamed subscriptions. let numberRemovedSubscriptions = 0; await realm.subscriptions.update((mutableSubs) => { numberRemovedSubscriptions = mutableSubs.removeUnnamed(); });
Remove All Subscriptions
To remove all subscriptions from the subscriptions set, execute a transaction on
the subscriptions set. Call removeAll() on the
MutableSubscriptionSet
within the transaction
realm.subscriptions.update((mutableSubs) => { mutableSubs.removeAll(); });
Performance Considerations
API Efficiency
Managing multiple subscriptions with the subscribe()
and unsubscribe()
APIs described in the Subscribe to Queries section is less efficient than
performing batch updates when you manually manage subscriptions.
For better performance when making multiple subscription changes, use the
subscriptions
API to update all the subscriptions in a single transaction. To learn how, see
Manually Manage Subscriptions.
Group Updates for Improved Performance
Every write transaction for a subscription set has a performance cost. If you need to make multiple updates to a Realm object during a session, consider keeping edited objects in memory until all changes are complete. This improves sync performance by only writing the complete and updated object to your realm instead of every change.
Flexible Sync RQL Requirements and Limitations
Indexed Queryable Fields Subscription Requirements
Adding an indexed queryable field to
your App can improve performance for simple queries on data that is strongly
partitioned. For example, an app where queries strongly map data to a device,
store, or user, such as user_id == $0, “641374b03725038381d2e1fb”
, is
a good candidate for an indexed queryable field. However, an indexed
queryable field has specific requirements for use in a query subscription:
The indexed queryable field must be used in every subscription query. It cannot be missing from the query.
The indexed queryable field must use an
==
orIN
comparison against a constant at least once in the subscription query. For example,user_id == $0, "641374b03725038381d2e1fb"
orstore_id IN $0, {1,2,3}
.
You can optionally include an AND
comparison as long as the indexed
queryable field is directly compared against a constant using ==
or IN
at least once. For example, store_id IN {1,2,3} AND region=="Northeast"
or store_id == 1 AND (active_promotions < 5 OR num_employees < 10)
.
Invalid Flexible Sync queries on an indexed queryable field include queries where:
The indexed queryable field does not use
AND
with the rest of the query. For examplestore_id IN {1,2,3} OR region=="Northeast"
is invalid because it usesOR
instead ofAND
. Similarly,store_id == 1 AND active_promotions < 5 OR num_employees < 10
is invalid because theAND
only applies to the term next to it, not the entire query.The indexed queryable field is not used in an equality operator. For example
store_id > 2 AND region=="Northeast"
is invalid because it uses only the>
operator with the indexed queryable field and does not have an equality comparison.The query is missing the indexed queryable field entirely. For example,
region=="Northeast
ortruepredicate
are invalid because they do not contain the indexed queryable field.
Unsupported Query Operators in Flexible Sync
Flexible Sync has some limitations when using RQL operators. When you write the query subscription that determines which data to sync, the server does not support these query operators. However, you can still use the full range of RQL features to query the synced data set in the client application.
Operator Type | Unsupported Operators |
---|---|
Aggregate Operators | @avg , @count , @max , @min , @sum |
Query Suffixes | DISTINCT , SORT , LIMIT |
Case insensitive queries ([c]
) cannot use indexes effectively.
As a result, case insensitive queries are not recommended, since they could lead to
performance problems.
Flexible Sync only supports @count
for array fields.
List Queries
Flexible Sync supports querying lists using the IN
operator.
You can query a list of constants to see if it contains the value of a queryable field:
// Query a constant list for a queryable field value "priority IN { 1, 2, 3 }"
If a queryable field has an array value, you can query to see if it contains a constant value:
// Query an array-valued queryable field for a constant value "'comedy' IN genres"
Warning
You cannot compare two lists with each other in a Flexible Sync query. Note that this is valid Realm Query Language syntax outside of Flexible Sync queries.
// Invalid Flexible Sync query. Do not do this! "{'comedy', 'horror', 'suspense'} IN genres" // Another invalid Flexible Sync query. Do not do this! "ANY {'comedy', 'horror', 'suspense'} != ANY genres"
Embedded or Linked Objects
Flexible Sync does not support querying on properties in Embedded Objects
or links. For example, obj1.field == "foo"
.