Line data Source code
1 : /*
2 : * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 : #include "yrs_document.h"
18 :
19 : #include <atomic>
20 : #include <mutex>
21 :
22 : extern "C" {
23 : #include <libyrs.h>
24 : }
25 :
26 : namespace jami {
27 :
28 : struct YrsDocument::Impl
29 : {
30 : YDoc* doc {nullptr};
31 : std::mutex mutex;
32 : // Raised by the update observer whenever the document actually moves. It is
33 : // written from inside ytransaction_commit, with mutex already held by the
34 : // caller, so it must be lock-free and must never read the document back.
35 : std::atomic_bool changed {false};
36 : YSubscription* subscription {nullptr};
37 : };
38 :
39 33 : YrsDocument::YrsDocument(uint64_t clientId)
40 33 : : pimpl_(std::make_unique<Impl>())
41 : {
42 33 : YOptions options = yoptions();
43 33 : options.id = clientId;
44 : // UTF-16 offsets, matching what the Qt, Android and iOS text editors index
45 : // with. The daemon never uses an offset itself, but the flag is part of the
46 : // document's identity and every replica must agree on it.
47 33 : options.flags = Y_OFFSET_UTF16;
48 33 : pimpl_->doc = ydoc_new_with_options(options);
49 : // yoptions() hands over ownership of the strings it allocates (guid, and
50 : // collection_id when set), and ydoc_new_with_options() copies them rather
51 : // than adopting them. Without this, every document ever opened leaks its
52 : // guid for the lifetime of the daemon.
53 33 : if (options.guid)
54 33 : ystring_destroy(const_cast<char*>(options.guid));
55 33 : if (options.collection_id)
56 0 : ystring_destroy(const_cast<char*>(options.collection_id));
57 : // yrs only fires this when an update brings something the replica did not
58 : // already hold, which makes it the one honest answer to "did that change
59 : // anything". It says nothing about what changed, so the daemon stays as
60 : // blind to the document's shape as it was without it.
61 33 : pimpl_->subscription = ydoc_observe_updates_v1(pimpl_->doc, pimpl_.get(), [](void* state, uint32_t, const char*) {
62 13 : static_cast<Impl*>(state)->changed.store(true, std::memory_order_relaxed);
63 13 : });
64 33 : }
65 :
66 33 : YrsDocument::~YrsDocument()
67 : {
68 33 : if (pimpl_->subscription)
69 33 : yunobserve(pimpl_->subscription);
70 33 : if (pimpl_->doc)
71 33 : ydoc_destroy(pimpl_->doc);
72 33 : }
73 :
74 : bool
75 74 : YrsDocument::takeChanged()
76 : {
77 74 : return pimpl_->changed.exchange(false, std::memory_order_relaxed);
78 : }
79 :
80 : bool
81 18 : YrsDocument::applyUpdate(const Bytes& update)
82 : {
83 18 : if (update.empty())
84 0 : return false;
85 18 : std::lock_guard<std::mutex> lk(pimpl_->mutex);
86 18 : YTransaction* txn = ydoc_write_transaction(pimpl_->doc, 0, nullptr);
87 18 : if (!txn)
88 0 : return false;
89 : // Root types are created on demand by the update itself, so the document
90 : // never has to be told which shared types it holds.
91 18 : const auto err = ytransaction_apply(txn, reinterpret_cast<const char*>(update.data()), update.size());
92 18 : ytransaction_commit(txn);
93 18 : return err == 0;
94 18 : }
95 :
96 : YrsDocument::Bytes
97 26 : YrsDocument::encodeStateAsUpdate() const
98 : {
99 26 : std::lock_guard<std::mutex> lk(pimpl_->mutex);
100 26 : YTransaction* txn = ydoc_read_transaction(pimpl_->doc);
101 26 : if (!txn)
102 0 : return {};
103 26 : uint32_t len = 0;
104 : // A null state vector requests the full document state.
105 26 : char* data = ytransaction_state_diff_v1(txn, nullptr, 0, &len);
106 26 : Bytes update;
107 26 : if (data) {
108 26 : update.assign(reinterpret_cast<const uint8_t*>(data), reinterpret_cast<const uint8_t*>(data) + len);
109 26 : ybinary_destroy(data, len);
110 : }
111 26 : ytransaction_commit(txn);
112 26 : return update;
113 26 : }
114 :
115 : YrsDocument::Bytes
116 36 : YrsDocument::encodeStateVector() const
117 : {
118 36 : std::lock_guard<std::mutex> lk(pimpl_->mutex);
119 36 : YTransaction* txn = ydoc_read_transaction(pimpl_->doc);
120 36 : if (!txn)
121 0 : return {};
122 36 : uint32_t len = 0;
123 36 : char* data = ytransaction_state_vector_v1(txn, &len);
124 36 : Bytes sv;
125 36 : if (data) {
126 36 : sv.assign(reinterpret_cast<const uint8_t*>(data), reinterpret_cast<const uint8_t*>(data) + len);
127 36 : ybinary_destroy(data, len);
128 : }
129 36 : ytransaction_commit(txn);
130 36 : return sv;
131 36 : }
132 :
133 : YrsDocument::Bytes
134 4 : YrsDocument::encodeDiff(const Bytes& stateVector) const
135 : {
136 4 : if (stateVector.empty())
137 0 : return encodeStateAsUpdate();
138 4 : std::lock_guard<std::mutex> lk(pimpl_->mutex);
139 4 : YTransaction* txn = ydoc_read_transaction(pimpl_->doc);
140 4 : if (!txn)
141 0 : return {};
142 4 : uint32_t len = 0;
143 4 : char* data = ytransaction_state_diff_v1(txn,
144 4 : reinterpret_cast<const char*>(stateVector.data()),
145 4 : stateVector.size(),
146 : &len);
147 4 : Bytes update;
148 4 : if (data) {
149 4 : update.assign(reinterpret_cast<const uint8_t*>(data), reinterpret_cast<const uint8_t*>(data) + len);
150 4 : ybinary_destroy(data, len);
151 : }
152 4 : ytransaction_commit(txn);
153 4 : return update;
154 4 : }
155 :
156 : } // namespace jami
|