RLMSyncSubscription.mm 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. ////////////////////////////////////////////////////////////////////////////
  2. //
  3. // Copyright 2018 Realm Inc.
  4. //
  5. // Licensed under the Apache License, Version 2.0 (the "License");
  6. // you may not use this file except in compliance with the License.
  7. // You may obtain a copy of the License at
  8. //
  9. // http://www.apache.org/licenses/LICENSE-2.0
  10. //
  11. // Unless required by applicable law or agreed to in writing, software
  12. // distributed under the License is distributed on an "AS IS" BASIS,
  13. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. // See the License for the specific language governing permissions and
  15. // limitations under the License.
  16. //
  17. ////////////////////////////////////////////////////////////////////////////
  18. #import "RLMSyncSubscription.h"
  19. #import "RLMObjectSchema_Private.hpp"
  20. #import "RLMObject_Private.hpp"
  21. #import "RLMProperty_Private.hpp"
  22. #import "RLMRealm_Private.hpp"
  23. #import "RLMResults_Private.hpp"
  24. #import "RLMUtil.hpp"
  25. #import "object_store.hpp"
  26. #import "sync/partial_sync.hpp"
  27. using namespace realm;
  28. @implementation RLMSyncSubscriptionOptions
  29. @end
  30. @interface RLMSyncSubscription ()
  31. @property (nonatomic, readwrite) RLMSyncSubscriptionState state;
  32. @property (nonatomic, readwrite, nullable) NSError *error;
  33. @property (nonatomic, readwrite) NSString *query;
  34. @property (nonatomic, readwrite, nullable) NSDate *createdAt;
  35. @property (nonatomic, readwrite, nullable) NSDate *updatedAt;
  36. @property (nonatomic, readwrite, nullable) NSDate *expiresAt;
  37. @property (nonatomic, readwrite) NSTimeInterval timeToLive;
  38. @end
  39. @implementation RLMSyncSubscription {
  40. partial_sync::SubscriptionNotificationToken _token;
  41. util::Optional<partial_sync::Subscription> _subscription;
  42. Object _obj;
  43. RLMRealm *_realm;
  44. }
  45. static std::vector<LinkPathPart> parseKeypath(StringData keypath, Group const& group,
  46. Schema const& schema, const ObjectSchema *objectSchema) {
  47. auto check = [&](bool condition, const char* fmt, auto... args) {
  48. if (!condition) {
  49. throw std::invalid_argument(util::format("Invalid LinkingObjects inclusion from key path '%1': %2.",
  50. keypath, util::format(fmt, args...)));
  51. }
  52. };
  53. const char* begin = keypath.data();
  54. const char* end = keypath.data() + keypath.size();
  55. check(begin != end, "missing property name");
  56. std::vector<LinkPathPart> ret;
  57. while (begin != end) {
  58. auto sep = std::find(begin, end, '.');
  59. check(sep != begin && sep + 1 != end, "missing property name");
  60. StringData key(begin, sep - begin);
  61. begin = sep + (sep != end);
  62. auto prop = objectSchema->property_for_name(key);
  63. check(prop, "property '%1.%2' does not exist", objectSchema->name, key);
  64. check(prop->type == PropertyType::Object || prop->type == PropertyType::LinkingObjects,
  65. "property '%1.%2' is of unsupported type '%3'",
  66. objectSchema->name, key, string_for_property_type(prop->type));
  67. objectSchema = &*schema.find(prop->object_type);
  68. if (prop->type == PropertyType::Object) {
  69. check(begin != end, "key path must end in a LinkingObjects property and '%1.%2' is of type '%3'",
  70. objectSchema->name, key, string_for_property_type(prop->type));
  71. ret.emplace_back(prop->table_column);
  72. }
  73. else {
  74. ret.emplace_back(objectSchema->property_for_name(prop->link_origin_property_name)->table_column,
  75. ObjectStore::table_for_object_type(group, objectSchema->name));
  76. }
  77. }
  78. return ret;
  79. }
  80. - (instancetype)initWithOptions:(RLMSyncSubscriptionOptions *)options results:(Results const&)results realm:(RLMRealm *)realm {
  81. if (!(self = [super init]))
  82. return nil;
  83. _name = [options.name copy];
  84. _timeToLive = NAN;
  85. _realm = realm;
  86. _createdAt = _updatedAt = NSDate.date;
  87. try {
  88. partial_sync::SubscriptionOptions opt;
  89. if (options.name) {
  90. opt.user_provided_name = std::string(RLMStringDataWithNSString(options.name));
  91. }
  92. if (options.timeToLive > 0) {
  93. opt.time_to_live_ms = options.timeToLive * 1000;
  94. }
  95. opt.update = options.overwriteExisting;
  96. if (options.includeLinkingObjectProperties) {
  97. std::vector<std::vector<LinkPathPart>> keypaths;
  98. for (NSString *keyPath in options.includeLinkingObjectProperties) {
  99. keypaths.push_back(parseKeypath(keyPath.UTF8String, realm.group,
  100. realm->_realm->schema(),
  101. &results.get_object_schema()));
  102. }
  103. opt.inclusions = IncludeDescriptor{*ObjectStore::table_for_object_type(realm.group, results.get_object_type()), keypaths};
  104. }
  105. _subscription = partial_sync::subscribe(options.limit ? results.limit(options.limit) : results, std::move(opt));
  106. }
  107. catch (std::exception const& e) {
  108. @throw RLMException(e);
  109. }
  110. self.state = (RLMSyncSubscriptionState)_subscription->state();
  111. __weak auto weakSelf = self;
  112. _token = _subscription->add_notification_callback([weakSelf] {
  113. RLMSyncSubscription *self;
  114. @autoreleasepool {
  115. self = weakSelf;
  116. if (!self)
  117. return;
  118. }
  119. // Retrieve the current error and status. Update our properties only if the values have changed,
  120. // since clients use KVO to observe these properties.
  121. if (auto error = self->_subscription->error()) {
  122. try {
  123. std::rethrow_exception(error);
  124. } catch (...) {
  125. NSError *nsError;
  126. RLMRealmTranslateException(&nsError);
  127. if (!self.error || ![self.error isEqual:nsError])
  128. self.error = nsError;
  129. }
  130. }
  131. else if (self.error) {
  132. self.error = nil;
  133. }
  134. auto status = (RLMSyncSubscriptionState)self->_subscription->state();
  135. if (status != self.state) {
  136. if (status == RLMSyncSubscriptionStateCreating) {
  137. // If a subscription is deleted without going through this
  138. // object's unsubscribe() method the subscription will transition
  139. // back to Creating rather than Invalidated since it doesn't
  140. // have a good way to track that it previously existed
  141. if (self.state != RLMSyncSubscriptionStateInvalidated)
  142. self.state = RLMSyncSubscriptionStateInvalidated;
  143. }
  144. else {
  145. self.state = status;
  146. }
  147. }
  148. if (status != RLMSyncSubscriptionStateComplete) {
  149. return;
  150. }
  151. auto obj = self->_subscription->result_set_object();
  152. if (obj && obj->is_valid()) {
  153. _obj = std::move(*obj);
  154. _token = {};
  155. _token.result_sets_token = _obj.add_notification_callback([weakSelf](CollectionChangeSet const&, std::exception_ptr) {
  156. @autoreleasepool {
  157. [weakSelf updateFromRow];
  158. }
  159. });
  160. [self updateFromRow];
  161. }
  162. });
  163. return self;
  164. }
  165. - (void)unsubscribe {
  166. partial_sync::unsubscribe(*_subscription);
  167. }
  168. - (void)updateFromRow {
  169. // We only want to call the setter if the value actually changed because of KVO
  170. #define REALM_SET_IF_CHANGED(prop, value) do { \
  171. auto newValue = value; \
  172. if (prop != newValue) { \
  173. prop = newValue; \
  174. } \
  175. } while (0)
  176. if (!_obj.is_valid()) {
  177. REALM_SET_IF_CHANGED(self.state, RLMSyncSubscriptionStateInvalidated);
  178. return;
  179. }
  180. auto row = _obj.row();
  181. REALM_SET_IF_CHANGED(self.query, RLMStringDataToNSString(row.get_string(row.get_column_index("query"))));
  182. REALM_SET_IF_CHANGED(self.createdAt, RLMTimestampToNSDate(row.get_timestamp(row.get_column_index("created_at"))));
  183. REALM_SET_IF_CHANGED(self.updatedAt, RLMTimestampToNSDate(row.get_timestamp(row.get_column_index("updated_at"))));
  184. REALM_SET_IF_CHANGED(self.expiresAt, RLMTimestampToNSDate(row.get_timestamp(row.get_column_index("expires_at"))));
  185. #undef REALM_SET_IF_CHANGED
  186. auto ttl = row.get<util::Optional<int64_t>>(row.get_column_index("time_to_live"));
  187. if (ttl && _timeToLive != *ttl / 1000.0) {
  188. self.timeToLive = *ttl / 1000.0;
  189. }
  190. else if (!ttl && !isnan(_timeToLive)) {
  191. self.timeToLive = NAN;
  192. }
  193. }
  194. @end
  195. @interface RLMSyncSubscriptionObject : RLMObjectBase
  196. @end
  197. @implementation RLMSyncSubscriptionObject {
  198. util::Optional<NotificationToken> _token;
  199. Object _obj;
  200. }
  201. - (NSString *)name {
  202. return _row.is_attached() ? RLMStringDataToNSString(_row.get_string(_row.get_column_index("name"))) : nil;
  203. }
  204. - (NSString *)query {
  205. return _row.is_attached() ? RLMStringDataToNSString(_row.get_string(_row.get_column_index("query"))) : nil;
  206. }
  207. - (RLMSyncSubscriptionState)state {
  208. if (!_row.is_attached()) {
  209. return RLMSyncSubscriptionStateInvalidated;
  210. }
  211. return (RLMSyncSubscriptionState)_row.get_int(_row.get_column_index("status"));
  212. }
  213. - (NSError *)error {
  214. if (!_row.is_attached()) {
  215. return nil;
  216. }
  217. StringData err = _row.get_string(_row.get_column_index("error_message"));
  218. if (!err.size()) {
  219. return nil;
  220. }
  221. return [NSError errorWithDomain:RLMErrorDomain
  222. code:RLMErrorFail
  223. userInfo:@{NSLocalizedDescriptionKey: RLMStringDataToNSString(err)}];
  224. }
  225. - (NSDate *)createdAt {
  226. return _row.is_attached() ? RLMTimestampToNSDate(_row.get_timestamp(_row.get_column_index("created_at"))) : nil;
  227. }
  228. - (NSDate *)updatedAt {
  229. return _row.is_attached() ? RLMTimestampToNSDate(_row.get_timestamp(_row.get_column_index("updated_at"))) : nil;
  230. }
  231. - (NSDate *)expiresAt {
  232. return _row.is_attached() ? RLMTimestampToNSDate(_row.get_timestamp(_row.get_column_index("expires_at"))) : nil;
  233. }
  234. - (NSTimeInterval)timeToLive {
  235. if (!_row.is_attached()) {
  236. return NAN;
  237. }
  238. auto columnIndex = _row.get_column_index("time_to_live");
  239. if (_row.is_null(columnIndex)) {
  240. return NAN;
  241. }
  242. return _row.get_int(columnIndex) / 1000.0;
  243. }
  244. - (NSString *)descriptionWithMaxDepth:(NSUInteger)depth {
  245. if (depth == 0) {
  246. return @"<Maximum depth exceeded>";
  247. }
  248. auto objectType = _row.get_string(_row.get_column_index("matches_property"));
  249. objectType = objectType.substr(0, objectType.size() - strlen("_matches"));
  250. return [NSString stringWithFormat:@"RLMSyncSubscription {\n\tname = %@\n\tobjectType = %@\n\tquery = %@\n\tstatus = %@\n\terror = %@\n\tcreatedAt = %@\n\tupdatedAt = %@\n\texpiresAt = %@\n\ttimeToLive = %@\n}",
  251. self.name, RLMStringDataToNSString(objectType),
  252. RLMStringDataToNSString(_row.get_string(_row.get_column_index("query"))),
  253. @(self.state), self.error, self.createdAt, self.updatedAt, self.expiresAt, @(self.timeToLive)];
  254. }
  255. - (void)unsubscribe {
  256. if (_row) {
  257. partial_sync::unsubscribe(Object(_realm->_realm, *_info->objectSchema, _row));
  258. }
  259. }
  260. - (void)addObserver:(id)observer
  261. forKeyPath:(NSString *)keyPath
  262. options:(NSKeyValueObservingOptions)options
  263. context:(void *)context {
  264. // Make the `state` property observable by using an object notifier to
  265. // trigger changes. The normal KVO mechanisms don't work for this class due
  266. // to it not being a normal part of the schema.
  267. if (!_token) {
  268. struct {
  269. __weak RLMSyncSubscriptionObject *weakSelf;
  270. void before(realm::CollectionChangeSet const&) {
  271. @autoreleasepool {
  272. [weakSelf willChangeValueForKey:@"state"];
  273. }
  274. }
  275. void after(realm::CollectionChangeSet const&) {
  276. @autoreleasepool {
  277. [weakSelf didChangeValueForKey:@"state"];
  278. }
  279. }
  280. void error(std::exception_ptr) {}
  281. } callback{self};
  282. _obj = Object(_realm->_realm, *_info->objectSchema, _row);
  283. _token = _obj.add_notification_callback(callback);
  284. }
  285. [super addObserver:observer forKeyPath:keyPath options:options context:context];
  286. }
  287. @end
  288. static ObjectSchema& addPublicNames(ObjectSchema& os) {
  289. using namespace partial_sync;
  290. os.property_for_name(property_created_at)->public_name = "createdAt";
  291. os.property_for_name(property_updated_at)->public_name = "updatedAt";
  292. os.property_for_name(property_expires_at)->public_name = "expiresAt";
  293. os.property_for_name(property_time_to_live)->public_name = "timeToLive";
  294. os.property_for_name(property_error_message)->public_name = "error";
  295. return os;
  296. }
  297. // RLMClassInfo stores pointers into the schema rather than owning the objects
  298. // it points to, so for a ClassInfo that's not part of the schema we need a
  299. // wrapper object that owns them
  300. RLMResultsSetInfo::RLMResultsSetInfo(__unsafe_unretained RLMRealm *const realm)
  301. : osObjectSchema(realm->_realm->read_group(), partial_sync::result_sets_type_name)
  302. , rlmObjectSchema([RLMObjectSchema objectSchemaForObjectStoreSchema:addPublicNames(osObjectSchema)])
  303. , info(realm, rlmObjectSchema, &osObjectSchema)
  304. {
  305. rlmObjectSchema.accessorClass = [RLMSyncSubscriptionObject class];
  306. }
  307. RLMClassInfo& RLMResultsSetInfo::get(__unsafe_unretained RLMRealm *const realm) {
  308. if (!realm->_resultsSetInfo) {
  309. realm->_resultsSetInfo = std::make_unique<RLMResultsSetInfo>(realm);
  310. }
  311. return realm->_resultsSetInfo->info;
  312. }
  313. @interface RLMSubscriptionResults : RLMResults
  314. @end
  315. @implementation RLMSubscriptionResults
  316. + (instancetype)resultsWithRealm:(RLMRealm *)realm {
  317. auto table = ObjectStore::table_for_object_type(realm->_realm->read_group(), partial_sync::result_sets_type_name);
  318. if (!table) {
  319. @throw RLMException(@"-[RLMRealm subscriptions] can only be called on a Realm using query-based sync");
  320. }
  321. // The server automatically adds a few subscriptions for the permissions
  322. // types which we want to hide. They're just an implementation detail and
  323. // deleting them won't work out well for the user.
  324. auto query = table->where().ends_with(table->get_column_index("matches_property"), "_matches");
  325. return [self resultsWithObjectInfo:RLMResultsSetInfo::get(realm)
  326. results:Results(realm->_realm, std::move(query))];
  327. }
  328. // These operations require a valid schema for the type. It's unclear how they
  329. // would be useful so it's probably not worth fixing this.
  330. - (RLMResults *)sortedResultsUsingDescriptors:(__unused NSArray<RLMSortDescriptor *> *)properties {
  331. @throw RLMException(@"Sorting subscription results is currently not implemented");
  332. }
  333. - (RLMResults *)distinctResultsUsingKeyPaths:(__unused NSArray<NSString *> *)keyPaths {
  334. @throw RLMException(@"Distincting subscription results is currently not implemented");
  335. }
  336. @end
  337. @implementation RLMResults (SyncSubscription)
  338. - (RLMSyncSubscription *)subscribe {
  339. return [[RLMSyncSubscription alloc] initWithOptions:nil results:_results realm:self.realm];
  340. }
  341. - (RLMSyncSubscription *)subscribeWithName:(NSString *)subscriptionName {
  342. auto options = [[RLMSyncSubscriptionOptions alloc] init];
  343. options.name = subscriptionName;
  344. return [[RLMSyncSubscription alloc] initWithOptions:options results:_results realm:self.realm];
  345. }
  346. - (RLMSyncSubscription *)subscribeWithName:(NSString *)subscriptionName limit:(NSUInteger)limit {
  347. auto options = [[RLMSyncSubscriptionOptions alloc] init];
  348. options.name = subscriptionName;
  349. options.limit = limit;
  350. return [[RLMSyncSubscription alloc] initWithOptions:options results:_results realm:self.realm];
  351. }
  352. - (RLMSyncSubscription *)subscribeWithOptions:(RLMSyncSubscriptionOptions *)options {
  353. return [[RLMSyncSubscription alloc] initWithOptions:options results:_results realm:self.realm];
  354. }
  355. @end
  356. @implementation RLMRealm (SyncSubscription)
  357. - (RLMResults<RLMSyncSubscription *> *)subscriptions {
  358. [self verifyThread];
  359. return [RLMSubscriptionResults resultsWithRealm:self];
  360. }
  361. - (nullable RLMSyncSubscription *)subscriptionWithName:(NSString *)name {
  362. [self verifyThread];
  363. auto& info = RLMResultsSetInfo::get(self);
  364. if (!info.table()) {
  365. @throw RLMException(@"-[RLMRealm subcriptionWithName:] can only be called on a Realm using query-based sync");
  366. }
  367. auto row = info.table()->find_first(info.table()->get_column_index("name"),
  368. RLMStringDataWithNSString(name));
  369. if (row == npos) {
  370. return nil;
  371. }
  372. RLMObjectBase *acc = RLMCreateManagedAccessor(info.rlmObjectSchema.accessorClass, self, &info);
  373. acc->_row = info.table()->get(row);
  374. return (RLMSyncSubscription *)acc;
  375. }
  376. @end
  377. RLMSyncSubscription *RLMCastToSyncSubscription(id obj) {
  378. return obj;
  379. }