Enhancements of subscription api (#2060)
* Adding Realm.subscriptions() and Realm.unsubscribe() * Update changelog * RN support
This commit is contained in:
parent
d6c1c62a47
commit
ffb48ec2ed
|
@ -1,7 +1,8 @@
|
|||
x.x.x Release notes (yyyy-MM-dd)
|
||||
=============================================================
|
||||
### Enhancements
|
||||
* None.
|
||||
* Added `Realm.subscriptions()` to query active query-based sync subscriptions. This method is in beta and might change in future releases. ([#2052](https://github.com/realm/realm-js/issues/2052))
|
||||
* Added `Realm.unsubscribe()` to unsubscribe by name an active query-based sync subscription. This method is in beta and might change in future releases. ([#2052](https://github.com/realm/realm-js/issues/2052))
|
||||
|
||||
### Fixed
|
||||
* <How to hit and notice issue? what was the impact?> ([#????](https://github.com/realm/realm-js/issues/????), since v?.?.?)
|
||||
|
@ -15,7 +16,7 @@ x.x.x Release notes (yyyy-MM-dd)
|
|||
### Internal
|
||||
* None.
|
||||
|
||||
2.19.0 Release notes (2018-10-9)
|
||||
2.19.0-rc.1 Release notes (2018-10-9)
|
||||
=============================================================
|
||||
### Enhancements
|
||||
* Added `SyncConfig.customQueryBasedSyncIdentifier` to allow customizing the identifier appended to the realm path when opening a query based Realm. This identifier is used to distinguish between query based Realms opened on different devices and by default Realm builds it as a combination of a user's id and a random string, allowing the same user to subscribe to different queries on different devices. In very rare cases, you may want to share query based Realms between devices and specifying the `customQueryBasedSyncIdentifier` allows you to do that.
|
||||
|
|
|
@ -331,6 +331,22 @@ class Realm {
|
|||
* @throws {Error} If anything in the provided `config` is invalid.
|
||||
*/
|
||||
static deleteFile(config) {}
|
||||
|
||||
/**
|
||||
* Get a list of subscriptions. THIS METHOD IS IN BETA AND MAY CHANGE IN FUTURE VERSIONS.
|
||||
* @param {string} name - Optional parameter to query for either a specific name or pattern (using
|
||||
* cards `?` and `*`).
|
||||
* @throws {Error} If `name` is not a string.
|
||||
* @returns an array of objects of (`name`, `objectType`, `query`).
|
||||
*/
|
||||
subscriptions(name) {}
|
||||
|
||||
/**
|
||||
* Unsubscribe a named subscription. THIS METHOD IS IN BETA AND MAY CHANGE IN FUTURE VERSIONS.
|
||||
* @param {string} name - The name of the subscription.
|
||||
* @throws {Error} If `name` is not a string or an empty string.
|
||||
*/
|
||||
unsubscribe(name) {}
|
||||
}
|
||||
/**
|
||||
* This describes the different options used to create a {@link Realm} instance.
|
||||
|
|
|
@ -27,10 +27,11 @@ export default class Subscription {
|
|||
|
||||
Object.defineProperties(Subscription.prototype, {
|
||||
error: { get: getterForProperty('error') },
|
||||
state: { get: getterForProperty('state') }
|
||||
state: { get: getterForProperty('state') },
|
||||
name: { get: getterForProperty('name') }
|
||||
});
|
||||
|
||||
// // Non-mutating methods:
|
||||
// Non-mutating methods:
|
||||
createMethods(Subscription.prototype, objectTypes.SUBSCRIPTION, [
|
||||
'unsubscribe',
|
||||
'addListener',
|
||||
|
|
|
@ -54,7 +54,7 @@ function waitForDownloadConfig(config) {
|
|||
|
||||
/**
|
||||
* Finds the permissions associated with a given Role or create them as needed.
|
||||
*
|
||||
*
|
||||
* @param {RealmObject} Container RealmObject holding the permission list.
|
||||
* @param {List<Realm.Permissions.Permission>} list of permissions.
|
||||
* @param {string} name of the role to find or create permissions for.
|
||||
|
@ -363,6 +363,30 @@ module.exports = function(realmConstructor) {
|
|||
});
|
||||
}
|
||||
|
||||
const ResultSets = function() {};
|
||||
ResultSets.schema = Object.freeze({
|
||||
name: '__ResultSets',
|
||||
properties: {
|
||||
name: { type: 'string', indexed: true },
|
||||
query: 'string',
|
||||
matches_property: 'string',
|
||||
status: 'int',
|
||||
error_message: 'string',
|
||||
query_parse_counter: 'int'
|
||||
}
|
||||
});
|
||||
|
||||
const subscriptionSchema = {
|
||||
'ResultSets': ResultSets
|
||||
};
|
||||
|
||||
if (!realmConstructor.Subscription) {
|
||||
Object.defineProperty(realmConstructor, 'Subscription', {
|
||||
value: subscriptionSchema,
|
||||
configurable: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Add instance methods to the Realm object that are only applied if Sync is
|
||||
Object.defineProperties(realmConstructor.prototype, getOwnPropertyDescriptors({
|
||||
permissions(arg) {
|
||||
|
@ -379,6 +403,55 @@ module.exports = function(realmConstructor) {
|
|||
return classPermissions[0];
|
||||
}
|
||||
},
|
||||
|
||||
subscriptions(name) {
|
||||
let all_subscriptions = this.objects('__ResultSets');
|
||||
if (name) {
|
||||
if (typeof(name) !== 'string') {
|
||||
throw new Error(`string expected - got ${typeof(name)}.`);
|
||||
}
|
||||
if (name.includes('*') || name.includes('?')) {
|
||||
all_subscriptions = all_subscriptions.filtered(`name LIKE '${name}'`);
|
||||
} else {
|
||||
all_subscriptions = all_subscriptions.filtered(`name == '${name}'`);
|
||||
}
|
||||
}
|
||||
let listOfSubscriptions = [];
|
||||
for (var subscription of all_subscriptions) {
|
||||
let matches_property = subscription['matches_property'];
|
||||
let sub = {
|
||||
name: subscription['name'],
|
||||
objectType: matches_property.substr(0, matches_property.length-8), // remove _matches
|
||||
query: subscription['query'],
|
||||
}
|
||||
listOfSubscriptions.push(sub);
|
||||
}
|
||||
return listOfSubscriptions;
|
||||
},
|
||||
|
||||
unsubscribe(name) {
|
||||
if (typeof(name) === 'string') {
|
||||
if (name !== '') {
|
||||
let named_subscriptions = this.objects('__ResultSets').filtered(`name == '${name}'`);
|
||||
if (named_subscriptions.length === 0) {
|
||||
return;
|
||||
}
|
||||
let doCommit = false;
|
||||
if (!this.isInTransaction) {
|
||||
this.beginTransaction();
|
||||
doCommit = true;
|
||||
}
|
||||
this.delete(named_subscriptions);
|
||||
if (doCommit) {
|
||||
this.commitTransaction();
|
||||
}
|
||||
} else {
|
||||
throw new Error('Non-empty string expected.');
|
||||
}
|
||||
} else {
|
||||
throw new Error(`string expected - got ${typeof(name)}.`);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -391,8 +464,8 @@ module.exports = function(realmConstructor) {
|
|||
* but still need to modify any input config.
|
||||
*/
|
||||
_constructor(config) {
|
||||
// Even though this runs code only available for Sync it requires some serious misconfiguration
|
||||
// for this to happen
|
||||
// Even though this runs code only available for Sync it requires some serious misconfiguration
|
||||
// for this to happen
|
||||
if (config && config.sync) {
|
||||
if (!Realm.Sync) {
|
||||
throw new Error("Realm is not compiled with Sync, but the configuration contains sync features.");
|
||||
|
@ -408,6 +481,7 @@ module.exports = function(realmConstructor) {
|
|||
addSchemaIfNeeded(config.schema, realmConstructor.Permissions.Realm);
|
||||
addSchemaIfNeeded(config.schema, realmConstructor.Permissions.Role);
|
||||
addSchemaIfNeeded(config.schema, realmConstructor.Permissions.User);
|
||||
addSchemaIfNeeded(config.schema, realmConstructor.Subscription.ResultSets);
|
||||
}
|
||||
}
|
||||
return config;
|
||||
|
|
|
@ -649,6 +649,12 @@ interface ProgressPromise extends Promise<Realm> {
|
|||
progress(callback: Realm.Sync.ProgressNotificationCallback): Promise<Realm>
|
||||
}
|
||||
|
||||
interface NamedSubscription {
|
||||
name: string,
|
||||
objectType: string,
|
||||
query: string
|
||||
}
|
||||
|
||||
declare class Realm {
|
||||
static defaultPath: string;
|
||||
|
||||
|
@ -819,6 +825,8 @@ declare class Realm {
|
|||
permissions() : Realm.Permissions.Realm;
|
||||
permissions(objectType: string | Realm.ObjectSchema | Function) : Realm.Permissions.Class;
|
||||
|
||||
subscriptions(name?: string): NamedSubscription[];
|
||||
unsubscribe(name: string): void;
|
||||
}
|
||||
|
||||
declare module 'realm' {
|
||||
|
|
|
@ -308,7 +308,7 @@ void ResultsClass<T>::subscribe(ContextType ctx, ObjectType this_object, Argumen
|
|||
}
|
||||
|
||||
auto subscription = partial_sync::subscribe(*results, subscription_name);
|
||||
return_value.set(SubscriptionClass<T>::create_instance(ctx, std::move(subscription)));
|
||||
return_value.set(SubscriptionClass<T>::create_instance(ctx, std::move(subscription), subscription_name));
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
|
@ -688,9 +688,10 @@ void SessionClass<T>::override_server(ContextType ctx, ObjectType this_object, A
|
|||
template<typename T>
|
||||
class Subscription : public partial_sync::Subscription {
|
||||
public:
|
||||
Subscription(partial_sync::Subscription s) : partial_sync::Subscription(std::move(s)) {}
|
||||
Subscription(partial_sync::Subscription s, util::Optional<std::string> name) : partial_sync::Subscription(std::move(s)), m_name(name) {}
|
||||
Subscription(Subscription &&) = default;
|
||||
|
||||
util::Optional<std::string> m_name;
|
||||
std::vector<std::pair<Protected<typename T::Function>, partial_sync::SubscriptionNotificationToken>> m_notification_tokens;
|
||||
};
|
||||
|
||||
|
@ -712,10 +713,11 @@ public:
|
|||
std::string const name = "Subscription";
|
||||
|
||||
static FunctionType create_constructor(ContextType);
|
||||
static ObjectType create_instance(ContextType, partial_sync::Subscription);
|
||||
static ObjectType create_instance(ContextType, partial_sync::Subscription, util::Optional<std::string>);
|
||||
|
||||
static void get_state(ContextType, ObjectType, ReturnValue &);
|
||||
static void get_error(ContextType, ObjectType, ReturnValue &);
|
||||
static void get_name(ContextType, ObjectType, ReturnValue &);
|
||||
|
||||
static void unsubscribe(ContextType, ObjectType, Arguments, ReturnValue &);
|
||||
static void add_listener(ContextType, ObjectType, Arguments, ReturnValue &);
|
||||
|
@ -724,7 +726,8 @@ public:
|
|||
|
||||
PropertyMap<T> const properties = {
|
||||
{"state", {wrap<get_state>, nullptr}},
|
||||
{"error", {wrap<get_error>, nullptr}}
|
||||
{"error", {wrap<get_error>, nullptr}},
|
||||
{"name", {wrap<get_name>, nullptr}},
|
||||
};
|
||||
|
||||
MethodMap<T> const methods = {
|
||||
|
@ -736,8 +739,8 @@ public:
|
|||
};
|
||||
|
||||
template<typename T>
|
||||
typename T::Object SubscriptionClass<T>::create_instance(ContextType ctx, partial_sync::Subscription subscription) {
|
||||
return create_object<T, SubscriptionClass<T>>(ctx, new Subscription<T>(std::move(subscription)));
|
||||
typename T::Object SubscriptionClass<T>::create_instance(ContextType ctx, partial_sync::Subscription subscription, util::Optional<std::string> name) {
|
||||
return create_object<T, SubscriptionClass<T>>(ctx, new Subscription<T>(std::move(subscription), name));
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
|
@ -762,6 +765,18 @@ void SubscriptionClass<T>::get_error(ContextType ctx, ObjectType object, ReturnV
|
|||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void SubscriptionClass<T>::get_name(ContextType ctx, ObjectType object, ReturnValue &return_value) {
|
||||
auto subscription = get_internal<T, SubscriptionClass<T>>(object);
|
||||
if (subscription->m_name == util::none) {
|
||||
// FIXME: should we reconstruct the name to match the one stored in __ResultSets?
|
||||
return_value.set_undefined();
|
||||
}
|
||||
else {
|
||||
return_value.set(subscription->m_name);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void SubscriptionClass<T>::unsubscribe(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {
|
||||
args.validate_maximum(0);
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit b0fc2814d9e6061ce5ba1da887aab6cfba4755ca
|
||||
Subproject commit 85cc4f3fc78fb905060489d6e51c2503d770393b
|
|
@ -289,7 +289,7 @@ module.exports = {
|
|||
let realm = new Realm(config);
|
||||
TestCase.assertTrue(realm.empty);
|
||||
|
||||
TestCase.assertEqual(realm.schema.length, 5);
|
||||
TestCase.assertEqual(realm.schema.length, 5 + 1); // 5 = see below, 1 = __ResultSets
|
||||
TestCase.assertEqual(realm.schema.filter(schema => schema.name === '__Class').length, 1);
|
||||
TestCase.assertEqual(realm.schema.filter(schema => schema.name === '__Permission').length, 1);
|
||||
TestCase.assertEqual(realm.schema.filter(schema => schema.name === '__Realm').length, 1);
|
||||
|
@ -354,7 +354,7 @@ module.exports = {
|
|||
Realm.deleteFile(config);
|
||||
// connecting with an empty schema should be possible, permission is added implicitly
|
||||
Realm.open(user.createConfiguration()).then((realm) => {
|
||||
return waitForUpload(realm);
|
||||
return waitForUpload(realm);
|
||||
}).then((realm) => {
|
||||
return waitForDownload(realm);
|
||||
}).then((realm) => {
|
||||
|
@ -381,7 +381,7 @@ module.exports = {
|
|||
return getPartialRealm().then(realm => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let realmPermissions = realm.permissions();
|
||||
TestCase.assertEqual(2, realm.objects('__Role').length); // [ "everyone", "__User:<xxx>" ]
|
||||
TestCase.assertEqual(2, realm.objects('__Role').length); // [ "everyone", "__User:<xxx>" ]
|
||||
realm.write(() => {
|
||||
let permissions = realmPermissions.findOrCreate("foo");
|
||||
TestCase.assertEqual("foo", permissions.role.name);
|
||||
|
@ -393,7 +393,7 @@ module.exports = {
|
|||
TestCase.assertFalse(permissions.canQuery);
|
||||
TestCase.assertFalse(permissions.canModifySchema);
|
||||
TestCase.assertFalse(permissions.canSetPermissions);
|
||||
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:<xxx>", "foo" ]
|
||||
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:<xxx>", "foo" ]
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
|
@ -406,7 +406,7 @@ module.exports = {
|
|||
realm.write(() => {
|
||||
realm.create('__Role', {'name':'foo'});
|
||||
});
|
||||
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ]
|
||||
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ]
|
||||
|
||||
let realmPermissions = realm.permissions();
|
||||
realm.write(() => {
|
||||
|
@ -419,7 +419,7 @@ module.exports = {
|
|||
TestCase.assertFalse(permissions.canQuery);
|
||||
TestCase.assertFalse(permissions.canModifySchema);
|
||||
TestCase.assertFalse(permissions.canSetPermissions);
|
||||
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ]
|
||||
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ]
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
|
@ -430,7 +430,7 @@ module.exports = {
|
|||
return getPartialRealm().then(realm => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let classPermissions = realm.permissions('__Class');
|
||||
TestCase.assertEqual(2, realm.objects('__Role').length); // [ "everyone", "__User:xxx" ]
|
||||
TestCase.assertEqual(2, realm.objects('__Role').length); // [ "everyone", "__User:xxx" ]
|
||||
realm.write(() => {
|
||||
let permissions = classPermissions.findOrCreate("foo");
|
||||
TestCase.assertEqual("foo", permissions.role.name);
|
||||
|
@ -442,7 +442,7 @@ module.exports = {
|
|||
TestCase.assertFalse(permissions.canQuery);
|
||||
TestCase.assertFalse(permissions.canModifySchema);
|
||||
TestCase.assertFalse(permissions.canSetPermissions);
|
||||
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ]
|
||||
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ]
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
|
|
|
@ -1264,6 +1264,7 @@ module.exports = {
|
|||
// FIXME: We need to test adding a property also calls the listener
|
||||
testSchemaUpdatesNewClass: function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let called = false;
|
||||
let realm1 = new Realm({ _cache: false });
|
||||
TestCase.assertTrue(realm1.empty);
|
||||
TestCase.assertEqual(realm1.schema.length, 0); // empty schema
|
||||
|
@ -1274,6 +1275,7 @@ module.exports = {
|
|||
TestCase.assertEqual(schema[0].name, 'TestObject');
|
||||
TestCase.assertEqual(realm1.schema.length, 1);
|
||||
TestCase.assertEqual(realm.schema[0].name, 'TestObject');
|
||||
called = true;
|
||||
});
|
||||
|
||||
const schema = [{
|
||||
|
@ -1291,7 +1293,11 @@ module.exports = {
|
|||
// in real world, a Realm will not be closed just after its
|
||||
// schema has been updated
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
if (called) {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
},
|
||||
|
|
|
@ -870,6 +870,12 @@ module.exports = {
|
|||
Realm.deleteFile(config);
|
||||
|
||||
realm = new Realm(config);
|
||||
|
||||
let listener_called = false;
|
||||
let schema_listener = function (realm, msg, newSchema) {
|
||||
listener_called = true;
|
||||
};
|
||||
realm.addListener('schema', schema_listener);
|
||||
const session = realm.syncSession;
|
||||
TestCase.assertInstanceOf(session, Realm.Sync.Session);
|
||||
TestCase.assertEqual(session.user.identity, user.identity);
|
||||
|
@ -881,7 +887,7 @@ module.exports = {
|
|||
var subscription1 = results1.subscribe();
|
||||
TestCase.assertEqual(subscription1.state, Realm.Sync.SubscriptionState.Creating);
|
||||
|
||||
var subscription2 = results2.subscribe();
|
||||
var subscription2 = results2.subscribe('foobar');
|
||||
TestCase.assertEqual(subscription2.state, Realm.Sync.SubscriptionState.Creating);
|
||||
|
||||
let called1 = false;
|
||||
|
@ -893,15 +899,11 @@ module.exports = {
|
|||
TestCase.assertEqual(collection.length, 1);
|
||||
TestCase.assertTrue(collection[0].name === 'Lassy 1', "The object is not synced correctly");
|
||||
results1.removeAllListeners();
|
||||
subscription1.unsubscribe();
|
||||
TestCase.assertUndefined(subscription1.name);
|
||||
called1 = true;
|
||||
});
|
||||
} else if (state === Realm.Sync.SubscriptionState.Invalidated) {
|
||||
subscription1.removeAllListeners();
|
||||
if (called1 && called2) {
|
||||
realm.close();
|
||||
resolve('Done');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -911,20 +913,50 @@ module.exports = {
|
|||
TestCase.assertEqual(collection.length, 1);
|
||||
TestCase.assertTrue(collection[0].name === 'Lassy 2', "The object is not synced correctly");
|
||||
results2.removeAllListeners();
|
||||
subscription2.unsubscribe();
|
||||
TestCase.assertEqual(subscription2.name, 'foobar');
|
||||
called2 = true;
|
||||
});
|
||||
} else if (state === Realm.Sync.SubscriptionState.Invalidated) {
|
||||
subscription2.removeAllListeners();
|
||||
if (called1 && called2) {
|
||||
realm.close();
|
||||
resolve('Done');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
reject("listeners never called");
|
||||
if (called1 && called2 && listener_called) {
|
||||
let listOfSubscriptions = realm.subscriptions();
|
||||
TestCase.assertArrayLength(listOfSubscriptions, 2 + 5); // 2 = the two subscriptions, 5 = the permissions classes
|
||||
TestCase.assertEqual(listOfSubscriptions[0]['name'], '[Dog] name == "Lassy 1" '); // the query is the default name; notice the trailing whitespace!
|
||||
TestCase.assertEqual(listOfSubscriptions[0]['query'], 'name == "Lassy 1" '); // notice the trailing whitespace!
|
||||
TestCase.assertEqual(listOfSubscriptions[0]['objectType'], 'Dog');
|
||||
TestCase.assertEqual(listOfSubscriptions[1]['name'], 'foobar');
|
||||
TestCase.assertEqual(listOfSubscriptions[1]['query'], 'name == "Lassy 2" '); // notice the trailing whitespace!
|
||||
TestCase.assertEqual(listOfSubscriptions[1]['objectType'], 'Dog');
|
||||
|
||||
listOfSubscriptions = realm.subscriptions('foobar');
|
||||
TestCase.assertArrayLength(listOfSubscriptions, 1);
|
||||
|
||||
listOfSubscriptions = realm.subscriptions('*bar');
|
||||
TestCase.assertArrayLength(listOfSubscriptions, 1);
|
||||
|
||||
listOfSubscriptions = realm.subscriptions('RABOOF');
|
||||
TestCase.assertArrayLength(listOfSubscriptions, 0);
|
||||
|
||||
subscription1.unsubscribe();
|
||||
realm.unsubscribe('foobar');
|
||||
realm.removeAllListeners();
|
||||
|
||||
// check if subscriptions have been removed
|
||||
// subscription1.unsubscribe() requires a server round-trip so it might take a while
|
||||
setTimeout(() => {
|
||||
listOfSubscriptions = realm.subscriptions();
|
||||
TestCase.assertArrayLength(listOfSubscriptions, 5); // the 5 permissions classes
|
||||
|
||||
realm.close();
|
||||
resolve();
|
||||
}, 10000);
|
||||
} else {
|
||||
reject("listeners never called");
|
||||
}
|
||||
}, 15000);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue