• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

realm / realm-core / 1892

05 Dec 2023 07:16PM UTC coverage: 91.692% (+0.02%) from 91.674%
1892

push

Evergreen

web-flow
Merge pull request #7161 from realm/tg/in-place-client-reset

Rewrite the local changesets in-place for client reset recovery

92346 of 169330 branches covered (0.0%)

835 of 865 new or added lines in 17 files covered. (96.53%)

103 existing lines in 16 files now uncovered.

231991 of 253012 relevant lines covered (91.69%)

6443355.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.49
/src/realm/sync/subscriptions.cpp
1
/*************************************************************************
2
 *
3
 * Copyright 2021 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

19
#include "realm/sync/subscriptions.hpp"
20

21
#include "external/json/json.hpp"
22

23
#include "realm/data_type.hpp"
24
#include "realm/keys.hpp"
25
#include "realm/list.hpp"
26
#include "realm/sort_descriptor.hpp"
27
#include "realm/sync/noinst/sync_metadata_schema.hpp"
28
#include "realm/table.hpp"
29
#include "realm/table_view.hpp"
30
#include "realm/transaction.hpp"
31
#include "realm/util/flat_map.hpp"
32

33
#include <algorithm>
34
#include <initializer_list>
35
#include <stdexcept>
36

37
namespace realm::sync {
38
namespace {
39
// Schema version history:
40
//   v2: Initial public beta.
41

42
constexpr static int c_flx_schema_version = 2;
43
constexpr static std::string_view c_flx_subscription_sets_table("flx_subscription_sets");
44
constexpr static std::string_view c_flx_subscriptions_table("flx_subscriptions");
45

46
constexpr static std::string_view c_flx_sub_sets_state_field("state");
47
constexpr static std::string_view c_flx_sub_sets_version_field("version");
48
constexpr static std::string_view c_flx_sub_sets_error_str_field("error");
49
constexpr static std::string_view c_flx_sub_sets_subscriptions_field("subscriptions");
50
constexpr static std::string_view c_flx_sub_sets_snapshot_version_field("snapshot_version");
51

52
constexpr static std::string_view c_flx_sub_id_field("id");
53
constexpr static std::string_view c_flx_sub_created_at_field("created_at");
54
constexpr static std::string_view c_flx_sub_updated_at_field("updated_at");
55
constexpr static std::string_view c_flx_sub_name_field("name");
56
constexpr static std::string_view c_flx_sub_object_class_field("object_class");
57
constexpr static std::string_view c_flx_sub_query_str_field("query");
58

59
using OptionalString = util::Optional<std::string>;
60

61
enum class SubscriptionStateForStorage : int64_t {
62
    // The subscription set has been persisted locally but has not been acknowledged by the server yet.
63
    Pending = 1,
64
    // The server is currently sending the initial state that represents this subscription set to the client.
65
    Bootstrapping = 2,
66
    // This subscription set is the active subscription set that is currently being synchronized with the server.
67
    Complete = 3,
68
    // An error occurred while processing this subscription set on the server. Check error_str() for details.
69
    Error = 4,
70
    // The last bootstrap message containing the initial state for this subscription set has been received. The
71
    // client is awaiting a mark message to mark this subscription as fully caught up to history.
72
    AwaitingMark = 6,
73
};
74

75
SubscriptionSet::State state_from_storage(int64_t value)
76
{
7,234✔
77
    switch (static_cast<SubscriptionStateForStorage>(value)) {
7,234✔
78
        case SubscriptionStateForStorage::Pending:
5,292✔
79
            return SubscriptionSet::State::Pending;
5,292✔
80
        case SubscriptionStateForStorage::Bootstrapping:
76✔
81
            return SubscriptionSet::State::Bootstrapping;
76✔
82
        case SubscriptionStateForStorage::AwaitingMark:
748✔
83
            return SubscriptionSet::State::AwaitingMark;
748✔
84
        case SubscriptionStateForStorage::Complete:
1,102✔
85
            return SubscriptionSet::State::Complete;
1,102✔
86
        case SubscriptionStateForStorage::Error:
16✔
87
            return SubscriptionSet::State::Error;
16✔
88
        default:
✔
89
            throw RuntimeError(ErrorCodes::InvalidArgument,
×
90
                               util::format("Invalid state for SubscriptionSet stored on disk: %1", value));
×
91
    }
7,234✔
92
}
7,234✔
93

94
int64_t state_to_storage(SubscriptionSet::State state)
95
{
28,060✔
96
    switch (state) {
28,060✔
97
        case SubscriptionSet::State::Pending:
10,166✔
98
            return static_cast<int64_t>(SubscriptionStateForStorage::Pending);
10,166✔
99
        case SubscriptionSet::State::Bootstrapping:
9,148✔
100
            return static_cast<int64_t>(SubscriptionStateForStorage::Bootstrapping);
9,148✔
101
        case SubscriptionSet::State::AwaitingMark:
4,580✔
102
            return static_cast<int64_t>(SubscriptionStateForStorage::AwaitingMark);
4,580✔
103
        case SubscriptionSet::State::Complete:
4,146✔
104
            return static_cast<int64_t>(SubscriptionStateForStorage::Complete);
4,146✔
105
        case SubscriptionSet::State::Error:
20✔
106
            return static_cast<int64_t>(SubscriptionStateForStorage::Error);
20✔
107
        default:
✔
108
            REALM_UNREACHABLE();
109
    }
28,060✔
110
}
28,060✔
111

112
size_t state_to_order(SubscriptionSet::State needle)
113
{
4,376✔
114
    using State = SubscriptionSet::State;
4,376✔
115
    switch (needle) {
4,376✔
116
        case State::Uncommitted:
24✔
117
            return 0;
24✔
118
        case State::Pending:
768✔
119
            return 1;
768✔
120
        case State::Bootstrapping:
56✔
121
            return 2;
56✔
122
        case State::AwaitingMark:
632✔
123
            return 3;
632✔
124
        case State::Complete:
2,892✔
125
            return 4;
2,892✔
126
        case State::Error:
✔
127
            return 5;
×
128
        case State::Superseded:
4✔
129
            return 6;
4✔
130
    }
×
131
    REALM_UNREACHABLE();
132
}
×
133

134
template <typename T, typename Predicate>
135
void splice_if(std::list<T>& src, std::list<T>& dst, Predicate pred)
136
{
3,480✔
137
    for (auto it = src.begin(); it != src.end();) {
5,456✔
138
        if (pred(*it)) {
1,976✔
139
            dst.splice(dst.end(), src, it++);
728✔
140
        }
728✔
141
        else {
1,248✔
142
            ++it;
1,248✔
143
        }
1,248✔
144
    }
1,976✔
145
}
3,480✔
146

147
} // namespace
148

149
Subscription::Subscription(const SubscriptionStore* parent, Obj obj)
150
    : id(obj.get<ObjectId>(parent->m_sub_id))
151
    , created_at(obj.get<Timestamp>(parent->m_sub_created_at))
152
    , updated_at(obj.get<Timestamp>(parent->m_sub_updated_at))
153
    , name(obj.is_null(parent->m_sub_name) ? OptionalString(util::none)
154
                                           : OptionalString{obj.get<String>(parent->m_sub_name)})
155
    , object_class_name(obj.get<String>(parent->m_sub_object_class_name))
156
    , query_string(obj.get<String>(parent->m_sub_query_str))
157
{
3,990✔
158
}
3,990✔
159

160
Subscription::Subscription(util::Optional<std::string> name, std::string object_class_name, std::string query_str)
161
    : id(ObjectId::gen())
162
    , created_at(std::chrono::system_clock::now())
163
    , updated_at(created_at)
164
    , name(std::move(name))
165
    , object_class_name(std::move(object_class_name))
166
    , query_string(std::move(query_str))
167
{
1,026✔
168
}
1,026✔
169

170

171
SubscriptionSet::SubscriptionSet(std::weak_ptr<SubscriptionStore> mgr, const Transaction& tr, const Obj& obj,
172
                                 MakingMutableCopy making_mutable_copy)
173
    : m_mgr(mgr)
174
    , m_cur_version(tr.get_version())
175
    , m_version(obj.get_primary_key().get_int())
176
    , m_obj_key(obj.get_key())
177
{
6,182✔
178
    REALM_ASSERT(obj.is_valid());
6,182✔
179
    if (!making_mutable_copy) {
6,182✔
180
        load_from_database(obj);
4,992✔
181
    }
4,992✔
182
}
6,182✔
183

184
SubscriptionSet::SubscriptionSet(std::weak_ptr<SubscriptionStore> mgr, int64_t version, SupersededTag)
185
    : m_mgr(mgr)
186
    , m_version(version)
187
    , m_state(State::Superseded)
188
{
24✔
189
}
24✔
190

191
void SubscriptionSet::load_from_database(const Obj& obj)
192
{
4,992✔
193
    auto mgr = get_flx_subscription_store(); // Throws
4,992✔
194

2,496✔
195
    m_state = state_from_storage(obj.get<int64_t>(mgr->m_sub_set_state));
4,992✔
196
    m_error_str = obj.get<String>(mgr->m_sub_set_error_str);
4,992✔
197
    m_snapshot_version = static_cast<DB::version_type>(obj.get<int64_t>(mgr->m_sub_set_snapshot_version));
4,992✔
198
    auto sub_list = obj.get_linklist(mgr->m_sub_set_subscriptions);
4,992✔
199
    m_subs.clear();
4,992✔
200
    for (size_t idx = 0; idx < sub_list.size(); ++idx) {
8,982✔
201
        m_subs.push_back(Subscription(mgr.get(), sub_list.get_object(idx)));
3,990✔
202
    }
3,990✔
203
}
4,992✔
204

205
std::shared_ptr<SubscriptionStore> SubscriptionSet::get_flx_subscription_store() const
206
{
8,208✔
207
    if (auto mgr = m_mgr.lock()) {
8,208✔
208
        return mgr;
8,204✔
209
    }
8,204✔
210
    throw RuntimeError(ErrorCodes::BrokenInvariant, "Active SubscriptionSet without a SubscriptionStore");
4✔
211
}
4✔
212

213
int64_t SubscriptionSet::version() const
214
{
8,910✔
215
    return m_version;
8,910✔
216
}
8,910✔
217

218
DB::version_type SubscriptionSet::snapshot_version() const
219
{
884✔
220
    return m_snapshot_version;
884✔
221
}
884✔
222

223
SubscriptionSet::State SubscriptionSet::state() const
224
{
1,092✔
225
    return m_state;
1,092✔
226
}
1,092✔
227

228
StringData SubscriptionSet::error_str() const
229
{
2,026✔
230
    if (m_error_str.empty()) {
2,026✔
231
        return StringData{};
2,018✔
232
    }
2,018✔
233
    return m_error_str;
8✔
234
}
8✔
235

236
size_t SubscriptionSet::size() const
237
{
304✔
238
    return m_subs.size();
304✔
239
}
304✔
240

241
const Subscription& SubscriptionSet::at(size_t index) const
242
{
32✔
243
    return m_subs.at(index);
32✔
244
}
32✔
245

246
SubscriptionSet::const_iterator SubscriptionSet::begin() const
247
{
3,608✔
248
    return m_subs.begin();
3,608✔
249
}
3,608✔
250

251
SubscriptionSet::const_iterator SubscriptionSet::end() const
252
{
4,690✔
253
    return m_subs.end();
4,690✔
254
}
4,690✔
255

256
const Subscription* SubscriptionSet::find(StringData name) const
257
{
64✔
258
    for (auto&& sub : *this) {
92✔
259
        if (sub.name == name)
92✔
260
            return &sub;
48✔
261
    }
92✔
262
    return nullptr;
40✔
263
}
64✔
264

265
const Subscription* SubscriptionSet::find(const Query& query) const
266
{
48✔
267
    const auto query_desc = query.get_description();
48✔
268
    const auto table_name = Group::table_name_to_class_name(query.get_table()->get_name());
48✔
269
    for (auto&& sub : *this) {
60✔
270
        if (sub.object_class_name == table_name && sub.query_string == query_desc)
60✔
271
            return &sub;
48✔
272
    }
60✔
273
    return nullptr;
24✔
274
}
48✔
275

276
MutableSubscriptionSet::MutableSubscriptionSet(std::weak_ptr<SubscriptionStore> mgr, TransactionRef tr, Obj obj)
277
    : SubscriptionSet(mgr, *tr, obj, MakingMutableCopy{true})
278
    , m_tr(std::move(tr))
279
    , m_obj(std::move(obj))
280
{
1,190✔
281
}
1,190✔
282

283
void MutableSubscriptionSet::check_is_mutable() const
284
{
2,034✔
285
    if (m_tr->get_transact_stage() != DB::transact_Writing) {
2,034✔
286
        throw WrongTransactionState("Not a write transaction");
12✔
287
    }
12✔
288
}
2,034✔
289

290
// This uses the 'swap and pop' idiom to run in constant time.
291
// The iterator returned is:
292
//  1. end(), if the last subscription is removed
293
//  2. same iterator it is passed (but pointing to the last subscription in set), otherwise
294
MutableSubscriptionSet::iterator MutableSubscriptionSet::erase(const_iterator it)
295
{
56✔
296
    check_is_mutable();
56✔
297
    REALM_ASSERT(it != end());
56✔
298
    if (it == std::prev(m_subs.end())) {
56✔
299
        m_subs.pop_back();
24✔
300
        return end();
24✔
301
    }
24✔
302
    auto back = std::prev(m_subs.end());
32✔
303
    // const_iterator to iterator in constant time (See https://stackoverflow.com/a/10669041)
16✔
304
    auto iterator = m_subs.erase(it, it);
32✔
305
    std::swap(*iterator, *back);
32✔
306
    m_subs.pop_back();
32✔
307
    return iterator;
32✔
308
}
32✔
309

310
bool MutableSubscriptionSet::erase(StringData name)
311
{
12✔
312
    check_is_mutable();
12✔
313
    auto ptr = find(name);
12✔
314
    if (!ptr)
12✔
315
        return false;
4✔
316
    auto it = m_subs.begin() + (ptr - &m_subs.front());
8✔
317
    erase(it);
8✔
318
    return true;
8✔
319
}
8✔
320

321
bool MutableSubscriptionSet::erase(const Query& query)
322
{
24✔
323
    check_is_mutable();
24✔
324
    auto ptr = find(query);
24✔
325
    if (!ptr)
24✔
326
        return false;
×
327
    auto it = m_subs.begin() + (ptr - &m_subs.front());
24✔
328
    erase(it);
24✔
329
    return true;
24✔
330
}
24✔
331

332
bool MutableSubscriptionSet::erase_by_class_name(StringData object_class_name)
333
{
16✔
334
    // TODO: Use std::erase_if when switching to C++20.
8✔
335
    auto it = std::remove_if(m_subs.begin(), m_subs.end(), [&object_class_name](const Subscription& sub) {
36✔
336
        return sub.object_class_name == object_class_name;
36✔
337
    });
36✔
338
    auto erased = end() - it;
16✔
339
    m_subs.erase(it, end());
16✔
340
    return erased > 0;
16✔
341
}
16✔
342

343
bool MutableSubscriptionSet::erase_by_id(ObjectId id)
344
{
12✔
345
    auto it = std::find_if(m_subs.begin(), m_subs.end(), [&id](const Subscription& sub) -> bool {
20✔
346
        return sub.id == id;
20✔
347
    });
20✔
348
    if (it == end()) {
12✔
349
        return false;
4✔
350
    }
4✔
351
    erase(it);
8✔
352
    return true;
8✔
353
}
8✔
354

355
void MutableSubscriptionSet::clear()
356
{
248✔
357
    check_is_mutable();
248✔
358
    m_subs.clear();
248✔
359
}
248✔
360

361
void MutableSubscriptionSet::insert_sub(const Subscription& sub)
362
{
544✔
363
    check_is_mutable();
544✔
364
    m_subs.push_back(sub);
544✔
365
}
544✔
366

367
std::pair<SubscriptionSet::iterator, bool>
368
MutableSubscriptionSet::insert_or_assign_impl(iterator it, util::Optional<std::string> name,
369
                                              std::string object_class_name, std::string query_str)
370
{
1,022✔
371
    check_is_mutable();
1,022✔
372
    if (it != end()) {
1,022✔
373
        auto& sub = m_subs[it - begin()];
40✔
374
        sub.object_class_name = std::move(object_class_name);
40✔
375
        sub.query_string = std::move(query_str);
40✔
376
        sub.updated_at = Timestamp{std::chrono::system_clock::now()};
40✔
377

20✔
378
        return {it, false};
40✔
379
    }
40✔
380
    it = m_subs.insert(m_subs.end(),
982✔
381
                       Subscription(std::move(name), std::move(object_class_name), std::move(query_str)));
982✔
382

490✔
383
    return {it, true};
982✔
384
}
982✔
385

386
std::pair<SubscriptionSet::iterator, bool> MutableSubscriptionSet::insert_or_assign(std::string_view name,
387
                                                                                    const Query& query)
388
{
188✔
389
    auto table_name = Group::table_name_to_class_name(query.get_table()->get_name());
188✔
390
    auto query_str = query.get_description();
188✔
391
    auto it = std::find_if(begin(), end(), [&](const Subscription& sub) {
168✔
392
        return sub.name == name;
148✔
393
    });
148✔
394

94✔
395
    return insert_or_assign_impl(it, std::string{name}, std::move(table_name), std::move(query_str));
188✔
396
}
188✔
397

398
std::pair<SubscriptionSet::iterator, bool> MutableSubscriptionSet::insert_or_assign(const Query& query)
399
{
834✔
400
    auto table_name = Group::table_name_to_class_name(query.get_table()->get_name());
834✔
401
    auto query_str = query.get_description();
834✔
402
    auto it = std::find_if(begin(), end(), [&](const Subscription& sub) {
528✔
403
        return (!sub.name && sub.object_class_name == table_name && sub.query_string == query_str);
224✔
404
    });
224✔
405

416✔
406
    return insert_or_assign_impl(it, util::none, std::move(table_name), std::move(query_str));
834✔
407
}
834✔
408

409
void MutableSubscriptionSet::import(SubscriptionSet&& src_subs)
410
{
128✔
411
    check_is_mutable();
128✔
412
    SubscriptionSet::import(std::move(src_subs));
128✔
413
}
128✔
414

415
void SubscriptionSet::import(SubscriptionSet&& src_subs)
416
{
128✔
417
    m_subs = std::move(src_subs.m_subs);
128✔
418
}
128✔
419

420
void MutableSubscriptionSet::set_state(State new_state)
421
{
56✔
422
    REALM_ASSERT(m_state == State::Uncommitted);
56✔
423
    m_state = new_state;
56✔
424
}
56✔
425

426
MutableSubscriptionSet SubscriptionSet::make_mutable_copy() const
427
{
1,190✔
428
    auto mgr = get_flx_subscription_store(); // Throws
1,190✔
429
    return mgr->make_mutable_copy(*this);
1,190✔
430
}
1,190✔
431

432
void SubscriptionSet::refresh()
433
{
44✔
434
    auto mgr = get_flx_subscription_store(); // Throws
44✔
435
    if (mgr->would_refresh(m_cur_version)) {
44✔
436
        *this = mgr->get_refreshed(m_obj_key, version());
40✔
437
    }
40✔
438
}
44✔
439

440
util::Future<SubscriptionSet::State> SubscriptionSet::get_state_change_notification(State notify_when) const
441
{
860✔
442
    auto mgr = get_flx_subscription_store(); // Throws
860✔
443

430✔
444
    util::CheckedLockGuard lk(mgr->m_pending_notifications_mutex);
860✔
445
    // If we've already been superseded by another version getting completed, then we should skip registering
430✔
446
    // a notification because it may never fire.
430✔
447
    if (mgr->m_min_outstanding_version > version()) {
860✔
448
        return util::Future<State>::make_ready(State::Superseded);
4✔
449
    }
4✔
450

428✔
451
    State cur_state = state();
856✔
452
    StringData err_str = error_str();
856✔
453

428✔
454
    // If there have been writes to the database since this SubscriptionSet was created, we need to fetch
428✔
455
    // the updated version from the DB to know the true current state and maybe return a ready future.
428✔
456
    if (m_cur_version < mgr->m_db->get_version_of_latest_snapshot()) {
856✔
457
        auto refreshed_self = mgr->get_refreshed(m_obj_key, version());
32✔
458
        cur_state = refreshed_self.state();
32✔
459
        err_str = refreshed_self.error_str();
32✔
460
    }
32✔
461
    // If we've already reached the desired state, or if the subscription is in an error state,
428✔
462
    // we can return a ready future immediately.
428✔
463
    if (cur_state == State::Error) {
856✔
464
        return util::Future<State>::make_ready(Status{ErrorCodes::SubscriptionFailed, err_str});
4✔
465
    }
4✔
466
    else if (state_to_order(cur_state) >= state_to_order(notify_when)) {
852✔
467
        return util::Future<State>::make_ready(cur_state);
72✔
468
    }
72✔
469

390✔
470
    // Otherwise, make a promise/future pair and add it to the list of pending notifications.
390✔
471
    auto [promise, future] = util::make_promise_future<State>();
780✔
472
    mgr->m_pending_notifications.emplace_back(version(), std::move(promise), notify_when);
780✔
473
    return std::move(future);
780✔
474
}
780✔
475

476
void SubscriptionSet::get_state_change_notification(
477
    State notify_when, util::UniqueFunction<void(util::Optional<State>, util::Optional<Status>)> cb) const
478
{
×
479
    get_state_change_notification(notify_when).get_async([cb = std::move(cb)](StatusWith<State> result) {
×
480
        if (result.is_ok()) {
×
481
            cb(result.get_value(), {});
×
482
        }
×
483
        else {
×
484
            cb({}, result.get_status());
×
485
        }
×
486
    });
×
487
}
×
488

489
void SubscriptionStore::process_notifications(State new_state, int64_t version, std::string_view error_str)
490
{
3,364✔
491
    std::list<SubscriptionStore::NotificationRequest> to_finish;
3,364✔
492
    {
3,364✔
493
        util::CheckedLockGuard lk(m_pending_notifications_mutex);
3,364✔
494
        splice_if(m_pending_notifications, to_finish, [&](auto& req) {
2,652✔
495
            return (req.version == version &&
1,944✔
496
                    (new_state == State::Error || state_to_order(new_state) >= state_to_order(req.notify_when))) ||
1,646✔
497
                   (new_state == State::Complete && req.version < version);
1,594✔
498
        });
1,944✔
499

1,680✔
500
        if (new_state == State::Complete) {
3,364✔
501
            m_min_outstanding_version = version;
1,422✔
502
        }
1,422✔
503
    }
3,364✔
504

1,680✔
505
    for (auto& req : to_finish) {
2,034✔
506
        if (new_state == State::Error && req.version == version) {
708✔
507
            req.promise.set_error({ErrorCodes::SubscriptionFailed, error_str});
20✔
508
        }
20✔
509
        else if (req.version < version) {
688✔
510
            req.promise.emplace_value(State::Superseded);
8✔
511
        }
8✔
512
        else {
680✔
513
            req.promise.emplace_value(new_state);
680✔
514
        }
680✔
515
    }
708✔
516
}
3,364✔
517

518
SubscriptionSet MutableSubscriptionSet::commit()
519
{
1,122✔
520
    if (m_tr->get_transact_stage() != DB::transact_Writing) {
1,122✔
NEW
521
        throw LogicError(ErrorCodes::WrongTransactionState, "SubscriptionSet has already been committed");
×
522
    }
×
523
    auto mgr = get_flx_subscription_store(); // Throws
1,122✔
524

560✔
525
    if (m_state == State::Uncommitted) {
1,122✔
526
        m_state = State::Pending;
1,066✔
527
    }
1,066✔
528
    m_obj.set(mgr->m_sub_set_snapshot_version, static_cast<int64_t>(m_tr->get_version()));
1,122✔
529

560✔
530
    auto obj_sub_list = m_obj.get_linklist(mgr->m_sub_set_subscriptions);
1,122✔
531
    obj_sub_list.clear();
1,122✔
532
    for (const auto& sub : m_subs) {
1,314✔
533
        auto new_sub = obj_sub_list.create_and_insert_linked_object(obj_sub_list.size());
1,314✔
534
        new_sub.set(mgr->m_sub_id, sub.id);
1,314✔
535
        new_sub.set(mgr->m_sub_created_at, sub.created_at);
1,314✔
536
        new_sub.set(mgr->m_sub_updated_at, sub.updated_at);
1,314✔
537
        if (sub.name) {
1,314✔
538
            new_sub.set(mgr->m_sub_name, StringData(*sub.name));
256✔
539
        }
256✔
540
        new_sub.set(mgr->m_sub_object_class_name, StringData(sub.object_class_name));
1,314✔
541
        new_sub.set(mgr->m_sub_query_str, StringData(sub.query_string));
1,314✔
542
    }
1,314✔
543
    m_obj.set(mgr->m_sub_set_state, state_to_storage(m_state));
1,122✔
544
    if (!m_error_str.empty()) {
1,122✔
UNCOV
545
        m_obj.set(mgr->m_sub_set_error_str, StringData(m_error_str));
×
UNCOV
546
    }
×
547

560✔
548
    const auto flx_version = version();
1,122✔
549
    m_tr->commit_and_continue_as_read();
1,122✔
550

560✔
551
    mgr->process_notifications(m_state, flx_version, std::string_view(error_str()));
1,122✔
552

560✔
553
    return mgr->get_refreshed(m_obj.get_key(), flx_version, m_tr->get_version_of_current_transaction());
1,122✔
554
}
1,122✔
555

556
std::string SubscriptionSet::to_ext_json() const
557
{
1,888✔
558
    if (m_subs.empty()) {
1,888✔
559
        return "{}";
736✔
560
    }
736✔
561

578✔
562
    util::FlatMap<std::string, std::vector<std::string>> table_to_query;
1,152✔
563
    for (const auto& sub : *this) {
1,320✔
564
        std::string table_name(sub.object_class_name);
1,320✔
565
        auto& queries_for_table = table_to_query.at(table_name);
1,320✔
566
        auto query_it = std::find(queries_for_table.begin(), queries_for_table.end(), sub.query_string);
1,320✔
567
        if (query_it != queries_for_table.end()) {
1,320✔
568
            continue;
8✔
569
        }
8✔
570
        queries_for_table.emplace_back(sub.query_string);
1,312✔
571
    }
1,312✔
572

578✔
573
    if (table_to_query.empty()) {
1,152✔
574
        return "{}";
×
575
    }
×
576

578✔
577
    // TODO this is pulling in a giant compile-time dependency. We should have a better way of escaping the
578✔
578
    // query strings into a json object.
578✔
579
    nlohmann::json output_json;
1,152✔
580
    for (auto& table : table_to_query) {
1,252✔
581
        // We want to make sure that the queries appear in some kind of canonical order so that if there are
628✔
582
        // two subscription sets with the same subscriptions in different orders, the server doesn't have to
628✔
583
        // waste a bunch of time re-running the queries for that table.
628✔
584
        std::stable_sort(table.second.begin(), table.second.end());
1,252✔
585

628✔
586
        bool is_first = true;
1,252✔
587
        std::ostringstream obuf;
1,252✔
588
        for (const auto& query_str : table.second) {
1,312✔
589
            if (!is_first) {
1,312✔
590
                obuf << " OR ";
60✔
591
            }
60✔
592
            is_first = false;
1,312✔
593
            obuf << "(" << query_str << ")";
1,312✔
594
        }
1,312✔
595
        output_json[table.first] = obuf.str();
1,252✔
596
    }
1,252✔
597

578✔
598
    return output_json.dump();
1,152✔
599
}
1,152✔
600

601
namespace {
602
class SubscriptionStoreInit : public SubscriptionStore {
603
public:
604
    explicit SubscriptionStoreInit(DBRef db)
605
        : SubscriptionStore(std::move(db))
606
    {
964✔
607
    }
964✔
608
};
609
} // namespace
610

611
SubscriptionStoreRef SubscriptionStore::create(DBRef db)
612
{
964✔
613
    return std::make_shared<SubscriptionStoreInit>(std::move(db));
964✔
614
}
964✔
615

616
SubscriptionStore::SubscriptionStore(DBRef db)
617
    : m_db(std::move(db))
618
{
964✔
619
    std::vector<SyncMetadataTable> internal_tables{
964✔
620
        {&m_sub_set_table,
964✔
621
         c_flx_subscription_sets_table,
964✔
622
         {&m_sub_set_version_num, c_flx_sub_sets_version_field, type_Int},
964✔
623
         {
964✔
624
             {&m_sub_set_state, c_flx_sub_sets_state_field, type_Int},
964✔
625
             {&m_sub_set_snapshot_version, c_flx_sub_sets_snapshot_version_field, type_Int},
964✔
626
             {&m_sub_set_error_str, c_flx_sub_sets_error_str_field, type_String, true},
964✔
627
             {&m_sub_set_subscriptions, c_flx_sub_sets_subscriptions_field, c_flx_subscriptions_table, true},
964✔
628
         }},
964✔
629
        {&m_sub_table,
964✔
630
         c_flx_subscriptions_table,
964✔
631
         SyncMetadataTable::IsEmbeddedTag{},
964✔
632
         {
964✔
633
             {&m_sub_id, c_flx_sub_id_field, type_ObjectId},
964✔
634
             {&m_sub_created_at, c_flx_sub_created_at_field, type_Timestamp},
964✔
635
             {&m_sub_updated_at, c_flx_sub_updated_at_field, type_Timestamp},
964✔
636
             {&m_sub_name, c_flx_sub_name_field, type_String, true},
964✔
637
             {&m_sub_object_class_name, c_flx_sub_object_class_field, type_String},
964✔
638
             {&m_sub_query_str, c_flx_sub_query_str_field, type_String},
964✔
639
         }},
964✔
640
    };
964✔
641

482✔
642
    auto tr = m_db->start_read();
964✔
643
    SyncMetadataSchemaVersions schema_versions(tr);
964✔
644

482✔
645
    if (auto schema_version = schema_versions.get_version_for(tr, internal_schema_groups::c_flx_subscription_store);
964✔
646
        !schema_version) {
964✔
647
        tr->promote_to_write();
816✔
648
        schema_versions.set_version_for(tr, internal_schema_groups::c_flx_subscription_store, c_flx_schema_version);
816✔
649
        create_sync_metadata_schema(tr, &internal_tables);
816✔
650
        tr->commit_and_continue_as_read();
816✔
651
    }
816✔
652
    else {
148✔
653
        if (*schema_version != c_flx_schema_version) {
148✔
654
            throw RuntimeError(ErrorCodes::UnsupportedFileFormatVersion,
×
655
                               "Invalid schema version for flexible sync metadata");
×
656
        }
×
657
        load_sync_metadata_schema(tr, &internal_tables);
148✔
658
    }
148✔
659

482✔
660
    // Make sure the subscription set table is properly initialized
482✔
661
    initialize_subscriptions_table(std::move(tr), false);
964✔
662
}
964✔
663

664
void SubscriptionStore::initialize_subscriptions_table(TransactionRef&& tr, bool clear_table)
665
{
984✔
666
    if (auto sub_sets = tr->get_table(m_sub_set_table); clear_table || sub_sets->is_empty()) {
984✔
667
        tr->promote_to_write();
836✔
668
        // If erase_table is true, clear out the sub_sets table
416✔
669
        if (clear_table) {
836✔
670
            sub_sets->clear();
20✔
671
        }
20✔
672
        // There should always be at least one subscription set so that the user can always wait
416✔
673
        // for synchronizationon on the result of get_latest().
416✔
674
        auto zero_sub = sub_sets->create_object_with_primary_key(Mixed{int64_t(0)});
836✔
675
        zero_sub.set(m_sub_set_state, static_cast<int64_t>(SubscriptionSet::State::Pending));
836✔
676
        zero_sub.set(m_sub_set_snapshot_version, tr->get_version());
836✔
677
        tr->commit();
836✔
678
    }
836✔
679
}
984✔
680

681
SubscriptionSet SubscriptionStore::get_latest()
682
{
1,538✔
683
    auto tr = m_db->start_frozen();
1,538✔
684
    auto sub_sets = tr->get_table(m_sub_set_table);
1,538✔
685
    // There should always be at least one SubscriptionSet - the zeroth subscription set for schema instructions.
768✔
686
    REALM_ASSERT(!sub_sets->is_empty());
1,538✔
687

768✔
688
    auto latest_id = sub_sets->max(sub_sets->get_primary_key_column())->get_int();
1,538✔
689
    auto latest_obj = sub_sets->get_object_with_primary_key(Mixed{latest_id});
1,538✔
690

768✔
691
    return SubscriptionSet(weak_from_this(), *tr, latest_obj);
1,538✔
692
}
1,538✔
693

694
SubscriptionSet SubscriptionStore::get_active()
695
{
1,328✔
696
    auto tr = m_db->start_frozen();
1,328✔
697
    return SubscriptionSet(weak_from_this(), *tr, get_active(*tr));
1,328✔
698
}
1,328✔
699

700
Obj SubscriptionStore::get_active(const Transaction& tr)
701
{
1,444✔
702
    auto sub_sets = tr.get_table(m_sub_set_table);
1,444✔
703
    // There should always be at least one SubscriptionSet - the zeroth subscription set for schema instructions.
724✔
704
    REALM_ASSERT(!sub_sets->is_empty());
1,444✔
705

724✔
706
    DescriptorOrdering descriptor_ordering;
1,444✔
707
    descriptor_ordering.append_sort(SortDescriptor{{{sub_sets->get_primary_key_column()}}, {false}});
1,444✔
708
    descriptor_ordering.append_limit(LimitDescriptor{1});
1,444✔
709
    auto res = sub_sets->where()
1,444✔
710
                   .equal(m_sub_set_state, state_to_storage(SubscriptionSet::State::Complete))
1,444✔
711
                   .Or()
1,444✔
712
                   .equal(m_sub_set_state, state_to_storage(SubscriptionSet::State::AwaitingMark))
1,444✔
713
                   .find_all(descriptor_ordering);
1,444✔
714

724✔
715
    // If there is no active subscription yet, return the zeroth subscription.
724✔
716
    if (res.is_empty()) {
1,444✔
717
        return sub_sets->get_object_with_primary_key(0);
800✔
718
    }
800✔
719
    return res.get_object(0);
644✔
720
}
644✔
721

722
SubscriptionStore::VersionInfo SubscriptionStore::get_version_info() const
723
{
1,164✔
724
    auto tr = m_db->start_read();
1,164✔
725
    auto sub_sets = tr->get_table(m_sub_set_table);
1,164✔
726
    // There should always be at least one SubscriptionSet - the zeroth subscription set for schema instructions.
582✔
727
    REALM_ASSERT(!sub_sets->is_empty());
1,164✔
728

582✔
729
    VersionInfo ret;
1,164✔
730
    ret.latest = sub_sets->max(sub_sets->get_primary_key_column())->get_int();
1,164✔
731
    DescriptorOrdering descriptor_ordering;
1,164✔
732
    descriptor_ordering.append_sort(SortDescriptor{{{sub_sets->get_primary_key_column()}}, {false}});
1,164✔
733
    descriptor_ordering.append_limit(LimitDescriptor{1});
1,164✔
734

582✔
735
    auto res = sub_sets->where()
1,164✔
736
                   .equal(m_sub_set_state, state_to_storage(SubscriptionSet::State::Complete))
1,164✔
737
                   .Or()
1,164✔
738
                   .equal(m_sub_set_state, state_to_storage(SubscriptionSet::State::AwaitingMark))
1,164✔
739
                   .find_all(descriptor_ordering);
1,164✔
740
    ret.active = res.is_empty() ? SubscriptionSet::EmptyVersion : res.get_object(0).get_primary_key().get_int();
948✔
741

582✔
742
    res = sub_sets->where()
1,164✔
743
              .equal(m_sub_set_state, state_to_storage(SubscriptionSet::State::AwaitingMark))
1,164✔
744
              .find_all(descriptor_ordering);
1,164✔
745
    ret.pending_mark = res.is_empty() ? SubscriptionSet::EmptyVersion : res.get_object(0).get_primary_key().get_int();
1,156✔
746

582✔
747
    return ret;
1,164✔
748
}
1,164✔
749

750
util::Optional<SubscriptionStore::PendingSubscription>
751
SubscriptionStore::get_next_pending_version(int64_t last_query_version) const
752
{
9,100✔
753
    auto tr = m_db->start_read();
9,100✔
754
    auto sub_sets = tr->get_table(m_sub_set_table);
9,100✔
755
    // There should always be at least one SubscriptionSet - the zeroth subscription set for schema instructions.
4,658✔
756
    REALM_ASSERT(!sub_sets->is_empty());
9,100✔
757

4,658✔
758
    DescriptorOrdering descriptor_ordering;
9,100✔
759
    descriptor_ordering.append_sort(SortDescriptor{{{sub_sets->get_primary_key_column()}}, {true}});
9,100✔
760
    auto res = sub_sets->where()
9,100✔
761
                   .greater(sub_sets->get_primary_key_column(), last_query_version)
9,100✔
762
                   .group()
9,100✔
763
                   .equal(m_sub_set_state, state_to_storage(SubscriptionSet::State::Pending))
9,100✔
764
                   .Or()
9,100✔
765
                   .equal(m_sub_set_state, state_to_storage(SubscriptionSet::State::Bootstrapping))
9,100✔
766
                   .end_group()
9,100✔
767
                   .find_all(descriptor_ordering);
9,100✔
768

4,658✔
769
    if (res.is_empty()) {
9,100✔
770
        return util::none;
7,504✔
771
    }
7,504✔
772

798✔
773
    auto obj = res.get_object(0);
1,596✔
774
    auto query_version = obj.get_primary_key().get_int();
1,596✔
775
    auto snapshot_version = obj.get<int64_t>(m_sub_set_snapshot_version);
1,596✔
776
    return PendingSubscription{query_version, static_cast<DB::version_type>(snapshot_version)};
1,596✔
777
}
1,596✔
778

779
std::vector<SubscriptionSet> SubscriptionStore::get_pending_subscriptions()
780
{
24✔
781
    std::vector<SubscriptionSet> subscriptions_to_recover;
24✔
782
    auto active_sub = get_active();
24✔
783
    auto cur_query_version = active_sub.version();
24✔
784
    // get a copy of the pending subscription sets since the active version
12✔
785
    while (auto next_pending = get_next_pending_version(cur_query_version)) {
72✔
786
        cur_query_version = next_pending->query_version;
48✔
787
        subscriptions_to_recover.push_back(get_by_version(cur_query_version));
48✔
788
    }
48✔
789
    return subscriptions_to_recover;
24✔
790
}
24✔
791

792
void SubscriptionStore::notify_all_state_change_notifications(Status status)
793
{
968✔
794
    util::CheckedUniqueLock lk(m_pending_notifications_mutex);
968✔
795
    auto to_finish = std::move(m_pending_notifications);
968✔
796
    lk.unlock();
968✔
797

484✔
798
    // Just complete/cancel the pending notifications - this function does not alter the
484✔
799
    // state of any pending subscriptions
484✔
800
    for (auto& req : to_finish) {
496✔
801
        req.promise.set_error(status);
24✔
802
    }
24✔
803
}
968✔
804

805
void SubscriptionStore::terminate()
806
{
20✔
807
    // Clear out and initialize the subscription store
10✔
808
    initialize_subscriptions_table(m_db->start_read(), true);
20✔
809

10✔
810
    util::CheckedUniqueLock lk(m_pending_notifications_mutex);
20✔
811
    auto to_finish = std::move(m_pending_notifications);
20✔
812
    m_min_outstanding_version = 0;
20✔
813
    lk.unlock();
20✔
814

10✔
815
    for (auto& req : to_finish) {
18✔
816
        req.promise.emplace_value(SubscriptionSet::State::Superseded);
16✔
817
    }
16✔
818
}
20✔
819

820
void SubscriptionStore::update_state(int64_t version, State new_state, std::optional<std::string_view> error_str)
821
{
2,242✔
822
    REALM_ASSERT(error_str.has_value() == (new_state == State::Error));
2,242✔
823
    REALM_ASSERT(new_state != State::Pending);
2,242✔
824
    REALM_ASSERT(new_state != State::Superseded);
2,242✔
825

1,120✔
826
    auto tr = m_db->start_write();
2,242✔
827
    auto sub_sets = tr->get_table(m_sub_set_table);
2,242✔
828
    auto obj = sub_sets->get_object_with_primary_key(version);
2,242✔
829
    if (!obj) {
2,242✔
830
        // This can happen either due to a bug in the sync client or due to the
831
        // server sending us an error message for an invalid query version. We
832
        // assume it is the latter here.
NEW
833
        throw RuntimeError(ErrorCodes::SyncProtocolInvariantFailed,
×
NEW
834
                           util::format("Invalid state update for nonexistent query version %1", version));
×
UNCOV
835
    }
×
836

1,120✔
837
    auto old_state = state_from_storage(obj.get<int64_t>(m_sub_set_state));
2,242✔
838
    switch (new_state) {
2,242✔
839
        case State::Error:
20✔
840
            if (old_state == State::Complete) {
20✔
NEW
841
                throw RuntimeError(ErrorCodes::SyncProtocolInvariantFailed,
×
NEW
842
                                   util::format("Received error '%1' for already-completed query version %2. This "
×
NEW
843
                                                "may be due to a queryable field being removed in the server-side "
×
NEW
844
                                                "configuration making the previous subscription set no longer valid.",
×
NEW
845
                                                *error_str, version));
×
NEW
846
            }
×
847
            break;
20✔
848

10✔
849
        case State::Bootstrapping:
448✔
850
        case State::AwaitingMark:
848✔
851
            REALM_ASSERT(old_state != State::Complete);
848✔
852
            REALM_ASSERT(old_state != State::Error);
848✔
853
            break;
848✔
854

424✔
855
        case State::Complete:
1,374✔
856
            supercede_prior_to(tr, version);
1,374✔
857
            break;
1,374✔
858

424✔
859
        case State::Uncommitted:
424✔
NEW
860
        case State::Superseded:
✔
NEW
861
        case State::Pending:
✔
862
            REALM_TERMINATE("Illegal new state for subscription set");
863
    }
2,242✔
864

1,120✔
865
    obj.set(m_sub_set_state, state_to_storage(new_state));
2,242✔
866
    obj.set(m_sub_set_error_str, error_str ? StringData(*error_str) : StringData());
2,232✔
867

1,120✔
868
    tr->commit();
2,242✔
869

1,120✔
870
    process_notifications(new_state, version, error_str.value_or(std::string_view{}));
2,242✔
871
}
2,242✔
872

873
SubscriptionSet SubscriptionStore::get_by_version(int64_t version_id)
874
{
956✔
875
    auto tr = m_db->start_frozen();
956✔
876
    auto sub_sets = tr->get_table(m_sub_set_table);
956✔
877
    if (auto obj = sub_sets->get_object_with_primary_key(version_id)) {
956✔
878
        return SubscriptionSet(weak_from_this(), *tr, obj);
952✔
879
    }
952✔
880

2✔
881
    util::CheckedLockGuard lk(m_pending_notifications_mutex);
4✔
882
    if (version_id < m_min_outstanding_version) {
4✔
883
        return SubscriptionSet(weak_from_this(), version_id, SubscriptionSet::SupersededTag{});
4✔
884
    }
4✔
885
    throw KeyNotFound(util::format("Subscription set with version %1 not found", version_id));
×
886
}
×
887

888
SubscriptionSet SubscriptionStore::get_refreshed(ObjKey key, int64_t version, std::optional<DB::VersionID> db_version)
889
{
1,194✔
890
    auto tr = m_db->start_frozen(db_version.value_or(VersionID{}));
1,194✔
891
    auto sub_sets = tr->get_table(m_sub_set_table);
1,194✔
892
    if (auto obj = sub_sets->try_get_object(key)) {
1,194✔
893
        return SubscriptionSet(weak_from_this(), *tr, obj);
1,174✔
894
    }
1,174✔
895
    return SubscriptionSet(weak_from_this(), version, SubscriptionSet::SupersededTag{});
20✔
896
}
20✔
897

898
SubscriptionStore::TableSet SubscriptionStore::get_tables_for_latest(const Transaction& tr) const
899
{
9,844✔
900
    auto sub_sets = tr.get_table(m_sub_set_table);
9,844✔
901
    // There should always be at least one SubscriptionSet - the zeroth subscription set for schema instructions.
4,914✔
902
    REALM_ASSERT(!sub_sets->is_empty());
9,844✔
903

4,914✔
904
    auto latest_id = sub_sets->max(sub_sets->get_primary_key_column())->get_int();
9,844✔
905
    auto latest_obj = sub_sets->get_object_with_primary_key(Mixed{latest_id});
9,844✔
906

4,914✔
907
    TableSet ret;
9,844✔
908
    auto subs = latest_obj.get_linklist(m_sub_set_subscriptions);
9,844✔
909
    for (size_t idx = 0; idx < subs.size(); ++idx) {
17,614✔
910
        auto sub_obj = subs.get_object(idx);
7,770✔
911
        ret.emplace(sub_obj.get<StringData>(m_sub_object_class_name));
7,770✔
912
    }
7,770✔
913

4,914✔
914
    return ret;
9,844✔
915
}
9,844✔
916

917
void SubscriptionStore::supercede_prior_to(TransactionRef tr, int64_t version_id) const
918
{
1,374✔
919
    auto sub_sets = tr->get_table(m_sub_set_table);
1,374✔
920
    Query remove_query(sub_sets);
1,374✔
921
    remove_query.less(sub_sets->get_primary_key_column(), version_id);
1,374✔
922
    remove_query.remove();
1,374✔
923
}
1,374✔
924

925
MutableSubscriptionSet SubscriptionStore::make_mutable_copy(const SubscriptionSet& set)
926
{
1,190✔
927
    auto new_tr = m_db->start_write();
1,190✔
928

594✔
929
    auto sub_sets = new_tr->get_table(m_sub_set_table);
1,190✔
930
    auto new_pk = sub_sets->max(sub_sets->get_primary_key_column())->get_int() + 1;
1,190✔
931

594✔
932
    MutableSubscriptionSet new_set_obj(weak_from_this(), std::move(new_tr),
1,190✔
933
                                       sub_sets->create_object_with_primary_key(Mixed{new_pk}));
1,190✔
934
    for (const auto& sub : set) {
842✔
935
        new_set_obj.insert_sub(sub);
496✔
936
    }
496✔
937

594✔
938
    return new_set_obj;
1,190✔
939
}
1,190✔
940

941
bool SubscriptionStore::would_refresh(DB::version_type version) const noexcept
942
{
40✔
943
    return version < m_db->get_version_of_latest_snapshot();
40✔
944
}
40✔
945

946
int64_t SubscriptionStore::set_active_as_latest(Transaction& wt)
947
{
48✔
948
    auto sub_sets = wt.get_table(m_sub_set_table);
48✔
949
    auto active = get_active(wt);
48✔
950
    // Delete all newer subscription sets, if any
24✔
951
    sub_sets->where().greater(sub_sets->get_primary_key_column(), active.get_primary_key().get_int()).remove();
48✔
952
    // Mark the active set as complete even if it was previously WaitingForMark
24✔
953
    // as we've completed rebootstrapping before calling this.
24✔
954
    active.set(m_sub_set_state, state_to_storage(State::Complete));
48✔
955
    auto version = active.get_primary_key().get_int();
48✔
956

24✔
957
    std::list<NotificationRequest> to_finish;
48✔
958
    {
48✔
959
        util::CheckedLockGuard lock(m_pending_notifications_mutex);
48✔
960
        splice_if(m_pending_notifications, to_finish, [&](auto& req) {
32✔
961
            if (req.version == version && state_to_order(req.notify_when) <= state_to_order(State::Complete))
16✔
962
                return true;
4✔
963
            return req.version != version;
12✔
964
        });
12✔
965
    }
48✔
966

24✔
967
    for (auto& req : to_finish) {
32✔
968
        req.promise.emplace_value(req.version == version ? State::Complete : State::Superseded);
14✔
969
    }
16✔
970

24✔
971
    return version;
48✔
972
}
48✔
973

974
int64_t SubscriptionStore::mark_active_as_complete(Transaction& wt)
975
{
68✔
976
    auto active = get_active(wt);
68✔
977
    active.set(m_sub_set_state, state_to_storage(State::Complete));
68✔
978
    auto version = active.get_primary_key().get_int();
68✔
979

34✔
980
    std::list<NotificationRequest> to_finish;
68✔
981
    {
68✔
982
        util::CheckedLockGuard lock(m_pending_notifications_mutex);
68✔
983
        splice_if(m_pending_notifications, to_finish, [&](auto& req) {
42✔
984
            return req.version == version && state_to_order(req.notify_when) <= state_to_order(State::Complete);
16✔
985
        });
16✔
986
    }
68✔
987

34✔
988
    for (auto& req : to_finish) {
36✔
989
        req.promise.emplace_value(State::Complete);
4✔
990
    }
4✔
991

34✔
992
    return version;
68✔
993
}
68✔
994

995
} // namespace realm::sync
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc