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 46 : YrsDocument::YrsDocument(uint64_t clientId)
40 46 : : pimpl_(std::make_unique<Impl>())
41 : {
42 46 : YOptions options = yoptions();
43 46 : 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 46 : options.flags = Y_OFFSET_UTF16;
48 46 : 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 46 : if (options.guid)
54 46 : ystring_destroy(const_cast<char*>(options.guid));
55 46 : 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 46 : pimpl_->subscription = ydoc_observe_updates_v1(pimpl_->doc, pimpl_.get(), [](void* state, uint32_t, const char*) {
62 26 : static_cast<Impl*>(state)->changed.store(true, std::memory_order_relaxed);
63 26 : });
64 46 : }
65 :
66 46 : YrsDocument::~YrsDocument()
67 : {
68 46 : if (pimpl_->subscription)
69 46 : yunobserve(pimpl_->subscription);
70 46 : if (pimpl_->doc)
71 46 : ydoc_destroy(pimpl_->doc);
72 46 : }
73 :
74 : bool
75 68 : YrsDocument::takeChanged()
76 : {
77 68 : return pimpl_->changed.exchange(false, std::memory_order_relaxed);
78 : }
79 :
80 : bool
81 32 : YrsDocument::applyUpdate(const Bytes& update)
82 : {
83 32 : if (update.empty())
84 0 : return false;
85 32 : std::lock_guard<std::mutex> lk(pimpl_->mutex);
86 32 : YTransaction* txn = ydoc_write_transaction(pimpl_->doc, 0, nullptr);
87 32 : 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 32 : const auto err = ytransaction_apply(txn, reinterpret_cast<const char*>(update.data()), update.size());
92 32 : ytransaction_commit(txn);
93 32 : return err == 0;
94 32 : }
95 :
96 : YrsDocument::Bytes
97 33 : YrsDocument::encodeStateAsUpdate() const
98 : {
99 33 : std::lock_guard<std::mutex> lk(pimpl_->mutex);
100 33 : YTransaction* txn = ydoc_read_transaction(pimpl_->doc);
101 33 : if (!txn)
102 0 : return {};
103 33 : uint32_t len = 0;
104 : // A null state vector requests the full document state.
105 33 : char* data = ytransaction_state_diff_v1(txn, nullptr, 0, &len);
106 33 : Bytes update;
107 33 : if (data) {
108 33 : update.assign(reinterpret_cast<const uint8_t*>(data), reinterpret_cast<const uint8_t*>(data) + len);
109 33 : ybinary_destroy(data, len);
110 : }
111 33 : ytransaction_commit(txn);
112 33 : return update;
113 33 : }
114 :
115 : YrsDocument::Bytes
116 33 : YrsDocument::encodeStateVector() const
117 : {
118 33 : std::lock_guard<std::mutex> lk(pimpl_->mutex);
119 33 : YTransaction* txn = ydoc_read_transaction(pimpl_->doc);
120 33 : if (!txn)
121 0 : return {};
122 33 : uint32_t len = 0;
123 33 : char* data = ytransaction_state_vector_v1(txn, &len);
124 33 : Bytes sv;
125 33 : if (data) {
126 33 : sv.assign(reinterpret_cast<const uint8_t*>(data), reinterpret_cast<const uint8_t*>(data) + len);
127 33 : ybinary_destroy(data, len);
128 : }
129 33 : ytransaction_commit(txn);
130 33 : return sv;
131 33 : }
132 :
133 : YrsDocument::Bytes
134 2 : YrsDocument::encodeDiff(const Bytes& stateVector) const
135 : {
136 2 : if (stateVector.empty())
137 0 : return encodeStateAsUpdate();
138 2 : std::lock_guard<std::mutex> lk(pimpl_->mutex);
139 2 : YTransaction* txn = ydoc_read_transaction(pimpl_->doc);
140 2 : if (!txn)
141 0 : return {};
142 2 : uint32_t len = 0;
143 2 : char* data = ytransaction_state_diff_v1(txn,
144 2 : reinterpret_cast<const char*>(stateVector.data()),
145 2 : stateVector.size(),
146 : &len);
147 2 : Bytes update;
148 2 : if (data) {
149 2 : update.assign(reinterpret_cast<const uint8_t*>(data), reinterpret_cast<const uint8_t*>(data) + len);
150 2 : ybinary_destroy(data, len);
151 : }
152 2 : ytransaction_commit(txn);
153 2 : return update;
154 2 : }
155 :
156 : } // namespace jami
|