17 #include <QJsonDocument>
19 #include <QMutexLocker>
20 #include <QStandardPaths>
32 using private_ns::CValuePage;
33 using private_ns::CDataPageQueue;
35 class CDataCacheRevision::LockGuard
38 LockGuard(
const LockGuard &) =
delete;
39 LockGuard &
operator=(
const LockGuard &) =
delete;
40 LockGuard(LockGuard &&other) noexcept : m_movedFrom(
true) { *
this = std::move(other); }
41 LockGuard &
operator=(LockGuard &&other) noexcept
43 auto tuple = std::tie(other.m_movedFrom, other.m_keepPromises, other.m_rev);
44 std::tie(m_movedFrom, m_keepPromises, m_rev).swap(tuple);
50 if (!m_movedFrom) { m_rev->finishUpdate(m_keepPromises); }
53 operator bool()
const {
return !m_movedFrom; }
56 LockGuard() : m_movedFrom(true) {}
58 LockGuard &keepPromises()
60 m_keepPromises =
true;
65 bool m_movedFrom =
false;
66 bool m_keepPromises =
false;
70 CDataCache::CDataCache() : CValueCache(1), m_serializer(new CDataCacheSerializer { this, revisionFileName() })
72 if (!QDir::root().mkpath(persistentStore()))
74 CLogMessage(
this).error(u
"Failed to create directory '%1'") << persistentStore();
77 connect(
this, &CValueCache::valuesChangedByLocal,
this, &CDataCache::saveToStoreAsync);
78 connect(
this, &CValueCache::valuesChangedByLocal,
this, [=](CValueCachePacket values) {
80 changeValuesFromRemote(values, CIdentifier());
82 connect(&m_watcher, &QFileSystemWatcher::fileChanged,
this, &CDataCache::loadFromStoreAsync);
83 connect(m_serializer, &CDataCacheSerializer::valuesLoadedFromStore,
this, &CDataCache::changeValuesFromRemote,
84 Qt::DirectConnection);
86 if (!QFile::exists(revisionFileName())) { QFile(revisionFileName()).open(QFile::WriteOnly); }
87 m_serializer->loadFromStore({},
false,
true);
91 m_serializer->start();
92 m_watcher.addPath(revisionFileName());
97 CDataCache::~CDataCache() { m_serializer->quitAndWait(); }
99 CDataCache *CDataCache::instance()
101 static std::unique_ptr<CDataCache> cache(
new CDataCache);
102 static auto dummy = (connect(qApp, &QObject::destroyed, cache.get(), [] { cache.reset(); }),
nullptr);
107 const QString &CDataCache::persistentStore()
109 static const QString dir = CFileUtils::appendFilePaths(getCacheRootDirectory(), relativeFilePath());
113 const QString &CDataCache::revisionFileName()
115 static const QString rev = CFileUtils::appendFilePaths(persistentStore(),
".rev");
119 QString CDataCache::filenameForKey(
const QString &key)
121 return CFileUtils::appendFilePaths(persistentStore(), instance()->CValueCache::filenameForKey(key));
124 QStringList CDataCache::enumerateStore()
const {
return enumerateFiles(persistentStore()); }
126 bool CDataCache::synchronize(
const QString &key)
128 constexpr
auto timeout = std::chrono::seconds(1);
129 constexpr
auto ready = std::future_status::ready;
130 constexpr
auto zero = std::chrono::seconds::zero();
132 std::future<void> future = m_revision.promiseLoadedValue(key, getTimestampSync(key));
135 std::future_status s {};
137 s = future.wait_for(timeout);
139 while (s != ready && m_revision.isNewerValueAvailable(key, getTimestampSync(key)));
140 if (s != ready) { s = future.wait_for(zero); }
141 if (s != ready) {
return false; }
151 catch (
const std::future_error &)
160 void CDataCache::setTimeToLive(
const QString &key,
int ttl)
162 singleShot(0, m_serializer, [
this, key, ttl] { m_revision.setTimeToLive(key, ttl); });
165 void CDataCache::renewTimestamp(
const QString &key, qint64 timestamp)
167 singleShot(0, m_serializer, [
this, key, timestamp] { m_revision.overrideTimestamp(key, timestamp); });
170 qint64 CDataCache::getTimestampOnDisk(
const QString &key) {
return m_revision.getTimestampOnDisk(key); }
172 void CDataCache::pinValue(
const QString &key)
174 singleShot(0, m_serializer, [
this, key] { m_revision.pinValue(key); });
177 void CDataCache::deferValue(
const QString &key)
179 singleShot(0, m_serializer, [
this, key] { m_revision.deferValue(key); });
182 void CDataCache::admitValue(
const QString &key,
bool triggerLoad)
184 m_revision.admitValue(key);
185 if (triggerLoad) { loadFromStoreAsync(); }
188 void CDataCache::sessionValue(
const QString &key)
190 singleShot(0, m_serializer, [
this, key] { m_revision.sessionValue(key); });
193 const QString &CDataCache::relativeFilePath()
195 static const QString p(
"/data/cache/core");
202 [
this, values] { m_serializer->saveToStore(values.
toVariantMap(), getAllValuesWithTimestamps()); });
205 void CDataCache::loadFromStoreAsync()
207 singleShot(0, m_serializer, [
this] { m_serializer->loadFromStore(getAllValuesWithTimestamps()); });
210 void CDataCache::connectPage(CValuePage *page)
212 auto *queue =
new CDataPageQueue(page);
213 connect(page, &CValuePage::valuesWantToCache,
this, &CDataCache::changeValues);
214 connect(
this, &CDataCache::valuesChanged, queue, &CDataPageQueue::queueValuesFromCache, Qt::DirectConnection);
217 void CDataPageQueue::queueValuesFromCache(
const CValueCachePacket &values, QObject *changedBy)
219 QMutexLocker lock(&m_mutex);
220 if (m_queue.isEmpty())
222 singleShot(0,
this, [
this] { setQueuedValuesFromCache(); });
224 m_queue.push_back(std::make_pair(values, changedBy));
227 void CDataPageQueue::setQueuedValuesFromCache()
229 QMutexLocker lock(&m_mutex);
230 decltype(m_queue) queue;
234 for (
const auto &pair : std::as_const(queue)) { m_page->setValuesFromCache(pair.first, pair.second); }
237 void CDataPageQueue::setQueuedValueFromCache(
const QString &key)
239 QMutexLocker lock(&m_mutex);
241 decltype(m_queue) filtered;
242 for (
auto &pair : m_queue)
244 if (pair.first.contains(key)) { filtered.push_back({ pair.first.takeByKey(key), pair.second }); }
247 for (
const auto &pair : filtered) { m_page->setValuesFromCache(pair.first, pair.second); }
250 const QStringList &CDataCacheSerializer::getLogCategories()
256 CDataCacheSerializer::CDataCacheSerializer(CDataCache *owner,
const QString &revisionFileName)
257 : CContinuousWorker(owner, QStringLiteral(
"CDataCacheSerializer '%1'").arg(revisionFileName)), m_cache(owner),
258 m_revisionFileName(revisionFileName)
261 const QString &CDataCacheSerializer::persistentStore()
const {
return m_cache->persistentStore(); }
266 m_cache->m_revision.notifyPendingWrite();
268 loadFromStore(baseline,
true);
269 for (
const auto &key : values.
keys())
271 m_deferredChanges.remove(key);
274 if (!lock) {
return; }
278 msg.setCategories(
this);
279 CLogMessage::preformatted(msg);
281 applyDeferredChanges();
284 CDataCacheRevision::LockGuard CDataCacheSerializer::loadFromStore(
const CValueCachePacket &baseline,
bool defer,
287 auto lock = m_cache->m_revision.beginUpdate(baseline.toTimestampMap(), !pinsOnly, pinsOnly);
288 if (lock && m_cache->m_revision.isPendingRead())
290 CValueCachePacket newValues;
291 if (!m_cache->m_revision.isFound())
293 m_cache->loadFromFiles(persistentStore(), {}, {}, newValues, {},
true);
294 m_cache->m_revision.regenerate(newValues);
298 m_cache->loadFromFiles(persistentStore(), m_cache->m_revision.keysWithNewerTimestamps(),
299 baseline.toVariantMap(), newValues, m_cache->m_revision.timestampsAsString());
300 newValues.setTimestamps(m_cache->m_revision.newerTimestamps());
302 auto missingKeys = m_cache->m_revision.keysWithNewerTimestamps() - newValues.keys();
303 if (!missingKeys.isEmpty()) { m_cache->m_revision.writeNewRevision({}, missingKeys); }
305 msg.setCategories(
this);
306 CLogMessage::preformatted(msg);
307 m_deferredChanges.insert(newValues);
310 if (!defer) { applyDeferredChanges(); }
314 void CDataCacheSerializer::applyDeferredChanges()
316 if (!m_deferredChanges.isEmpty())
318 m_deferredChanges.setSaved();
319 emit valuesLoadedFromStore(m_deferredChanges, CIdentifier::null());
320 deliverPromises(m_cache->m_revision.loadedValuePromises());
321 m_deferredChanges.clear();
325 void CDataCacheSerializer::deliverPromises(std::vector<std::promise<void>> i_promises)
328 [promises = std::make_shared<decltype(i_promises)>(std::move(i_promises))]() {
329 for (
auto &promise : *promises) { promise.set_value(); }
337 Session(
const QString &filename) : m_filename(filename) {}
338 void updateSession();
339 const QUuid &uuid()
const {
return m_uuid; }
342 const QString m_filename;
346 CDataCacheRevision::CDataCacheRevision(
const QString &basename)
347 : m_basename(basename), m_session(std::make_unique<Session>(m_basename +
"/.session"))
350 CDataCacheRevision::~CDataCacheRevision() =
default;
352 CDataCacheRevision::LockGuard CDataCacheRevision::beginUpdate(
const QMap<QString, qint64> ×tamps,
353 bool updateUuid,
bool pinsOnly)
355 QMutexLocker lock(&m_mutex);
357 Q_ASSERT(!m_updateInProgress);
358 Q_ASSERT(!m_lockFile.isLocked());
360 if (!m_lockFile.lock())
362 CLogMessage(
this).error(u
"Failed to lock %1: %2") << m_basename << CFileUtils::lockFileError(m_lockFile);
365 m_updateInProgress =
true;
366 LockGuard guard(
this);
368 m_timestamps.clear();
369 m_originalTimestamps.clear();
371 QFile revisionFile(CFileUtils::appendFilePaths(m_basename,
".rev"));
372 if ((m_found = revisionFile.exists()))
374 if (!revisionFile.open(QFile::ReadOnly | QFile::Text))
376 CLogMessage(
this).error(u
"Failed to open %1: %2")
377 << revisionFile.fileName() << revisionFile.errorString();
381 auto json = QJsonDocument::fromJson(revisionFile.readAll()).object();
382 if (json.contains(
"uuid") && json.contains(
"timestamps"))
384 m_originalTimestamps = fromJson(json.value(
"timestamps").toObject());
386 QUuid id(json.value(
"uuid").toString());
387 if (
id == m_uuid && m_admittedQueue.isEmpty())
389 if (m_pendingWrite) {
return guard; }
392 if (updateUuid) { m_uuid = id; }
394 auto timesToLive = fromJson(json.value(
"ttl").toObject());
395 for (
auto it = m_originalTimestamps.cbegin(); it != m_originalTimestamps.cend(); ++it)
397 auto current = timestamps.value(it.key(), -1);
398 auto ttl = timesToLive.value(it.key(), -1);
399 if (current < it.value() && (ttl < 0 || QDateTime::currentMSecsSinceEpoch() < it.value() + ttl))
401 m_timestamps.insert(it.key(), it.value());
404 if (m_timestamps.isEmpty())
406 if (m_pendingWrite) {
return guard; }
412 auto pins = fromJson(json.value(
"pins").toArray());
413 for (
const auto &key : m_timestamps.keys())
415 if (!pins.contains(key)) { m_timestamps.remove(key); }
419 auto deferrals = fromJson(json.value(
"deferrals").toArray());
420 m_admittedValues.unite(m_admittedQueue);
421 if (updateUuid) { m_admittedQueue.clear(); }
422 else if (!m_admittedQueue.isEmpty())
424 m_admittedQueue.intersect(QSet<QString>(m_timestamps.keyBegin(), m_timestamps.keyEnd()));
427 for (
const auto &key : m_timestamps.keys())
429 if (deferrals.contains(key) && !m_admittedValues.contains(key)) { m_timestamps.remove(key); }
432 m_session->updateSession();
433 auto sessionIds = sessionFromJson(json.value(
"session").toObject());
434 for (
auto it = sessionIds.cbegin(); it != sessionIds.cend(); ++it)
436 m_sessionValues[it.key()] = it.value();
437 if (it.value() != m_session->uuid())
439 m_timestamps.remove(it.key());
440 m_originalTimestamps.remove(it.key());
444 else if (revisionFile.size() > 0)
446 CLogMessage(
this).error(u
"Invalid format of %1") << revisionFile.fileName();
448 if (m_pendingWrite) {
return guard; }
451 else { m_found =
false; }
454 m_pendingRead =
true;
459 const QSet<QString> &excludeKeys)
461 QMutexLocker lock(&m_mutex);
463 Q_ASSERT(m_updateInProgress);
464 Q_ASSERT(m_lockFile.isLocked());
466 CAtomicFile revisionFile(CFileUtils::appendFilePaths(m_basename,
".rev"));
467 if (!revisionFile.open(QFile::WriteOnly | QFile::Text))
469 CLogMessage(
this).error(u
"Failed to open %1: %2") << revisionFile.fileName() << revisionFile.errorString();
473 m_uuid = CIdentifier().toUuid();
474 auto timestamps = m_originalTimestamps;
475 for (
auto it = i_timestamps.cbegin(); it != i_timestamps.cend(); ++it)
477 if (it.value()) { timestamps.insert(it.key(), it.value()); }
479 for (
const auto &key : excludeKeys) { timestamps.remove(key); }
481 for (
auto it = timestamps.cbegin(); it != timestamps.cend(); ++it)
483 if (m_sessionValues.contains(it.key())) { m_sessionValues[it.key()] = m_session->uuid(); }
487 json.insert(
"uuid", m_uuid.toString());
488 json.insert(
"timestamps", toJson(timestamps));
489 json.insert(
"ttl", toJson(m_timesToLive));
490 json.insert(
"pins", toJson(m_pinnedValues));
491 json.insert(
"deferrals", toJson(m_deferredValues));
492 json.insert(
"session", toJson(m_sessionValues));
493 revisionFile.write(QJsonDocument(json).toJson());
495 if (!revisionFile.checkedClose())
497 static const QString advice =
498 QStringLiteral(
"If this error persists, try restarting your computer or delete the file manually.");
499 CLogMessage(
this).error(u
"Failed to replace %1: %2 (%3)")
500 << revisionFile.fileName() << revisionFile.errorString() << advice;
504 void CDataCacheRevision::regenerate(
const CValueCachePacket &keys)
506 QMutexLocker lock(&m_mutex);
508 Q_ASSERT(m_updateInProgress);
509 Q_ASSERT(m_lockFile.isLocked());
511 writeNewRevision(m_originalTimestamps = keys.toTimestampMap());
514 void CDataCacheRevision::finishUpdate(
bool keepPromises)
516 QMutexLocker lock(&m_mutex);
518 Q_ASSERT(m_updateInProgress);
519 Q_ASSERT(m_lockFile.isLocked());
521 m_updateInProgress =
false;
522 m_pendingRead =
false;
523 m_pendingWrite =
false;
524 if (!keepPromises) { breakPromises(); }
528 bool CDataCacheRevision::isFound()
const
530 QMutexLocker lock(&m_mutex);
532 Q_ASSERT(m_updateInProgress);
536 bool CDataCacheRevision::isPendingRead()
const
538 QMutexLocker lock(&m_mutex);
540 Q_ASSERT(m_updateInProgress);
541 return !m_timestamps.isEmpty() || !m_found;
544 void CDataCacheRevision::notifyPendingWrite()
546 QMutexLocker lock(&m_mutex);
548 m_pendingWrite =
true;
551 QSet<QString> CDataCacheRevision::keysWithNewerTimestamps()
const
553 QMutexLocker lock(&m_mutex);
555 Q_ASSERT(m_updateInProgress);
556 return QSet<QString>(m_timestamps.keyBegin(), m_timestamps.keyEnd());
561 QMutexLocker lock(&m_mutex);
563 Q_ASSERT(m_updateInProgress);
567 bool CDataCacheRevision::isNewerValueAvailable(
const QString &key, qint64 timestamp)
569 QMutexLocker lock(&m_mutex);
574 return (m_updateInProgress || m_pendingWrite || beginUpdate({ { key, timestamp } },
false).keepPromises()) &&
575 (m_timestamps.contains(key) || m_admittedQueue.contains(key));
578 std::future<void> CDataCacheRevision::promiseLoadedValue(
const QString &key, qint64 currentTimestamp)
580 QMutexLocker lock(&m_mutex);
582 if (isNewerValueAvailable(key, currentTimestamp))
584 std::promise<void> promise;
585 auto future = promise.get_future();
586 m_promises.push_back(std::move(promise));
592 std::vector<std::promise<void>> CDataCacheRevision::loadedValuePromises()
594 QMutexLocker lock(&m_mutex);
596 Q_ASSERT(m_updateInProgress);
597 return std::move(m_promises);
600 void CDataCacheRevision::breakPromises()
602 QMutexLocker lock(&m_mutex);
604 if (!m_promises.empty())
606 CLogMessage(
this).debug() <<
"Breaking" << m_promises.size() <<
"promises";
611 QString CDataCacheRevision::timestampsAsString()
const
613 QMutexLocker lock(&m_mutex);
616 for (
auto it = m_timestamps.cbegin(); it != m_timestamps.cend(); ++it)
618 result.push_back(it.key() +
"(" +
619 QDateTime::fromMSecsSinceEpoch(it.value(), Qt::UTC).toString(Qt::ISODate) +
")");
621 return result.join(
",");
624 void CDataCacheRevision::setTimeToLive(
const QString &key,
int ttl)
626 QMutexLocker lock(&m_mutex);
628 Q_ASSERT(!m_updateInProgress);
629 m_timesToLive.insert(key, ttl);
632 void CDataCacheRevision::overrideTimestamp(
const QString &key, qint64 timestamp)
634 QMutexLocker lock(&m_mutex);
636 Q_ASSERT(!m_updateInProgress);
637 Q_ASSERT(!m_lockFile.isLocked());
639 if (!m_lockFile.lock())
641 CLogMessage(
this).error(u
"Failed to lock %1: %2") << m_basename << CFileUtils::lockFileError(m_lockFile);
646 CAtomicFile revisionFile(CFileUtils::appendFilePaths(m_basename,
".rev"));
647 if (revisionFile.exists())
649 if (!revisionFile.open(QFile::ReadWrite | QFile::Text))
651 CLogMessage(
this).error(u
"Failed to open %1: %2")
652 << revisionFile.fileName() << revisionFile.errorString();
657 auto json = QJsonDocument::fromJson(revisionFile.readAll()).object();
658 auto timestamps = json.value(
"timestamps").toObject();
659 timestamps.insert(key, timestamp);
660 json.insert(
"timestamps", timestamps);
662 if (revisionFile.seek(0) && revisionFile.resize(0) && revisionFile.write(QJsonDocument(json).toJson()))
664 if (!revisionFile.checkedClose())
666 static const QString advice = QStringLiteral(
667 "If this error persists, try restarting your computer or delete the file manually.");
668 CLogMessage(
this).error(u
"Failed to replace %1: %2 (%3)")
669 << revisionFile.fileName() << revisionFile.errorString() << advice;
674 CLogMessage(
this).error(u
"Failed to write to %1: %2")
675 << revisionFile.fileName() << revisionFile.errorString();
681 qint64 CDataCacheRevision::getTimestampOnDisk(
const QString &key)
683 QMutexLocker lock(&m_mutex);
685 if (m_lockFile.isLocked()) {
return m_originalTimestamps.value(key); }
687 if (!m_lockFile.lock())
689 CLogMessage(
this).error(u
"Failed to lock %1: %2") << m_basename << CFileUtils::lockFileError(m_lockFile);
695 QFile revisionFile(CFileUtils::appendFilePaths(m_basename,
".rev"));
696 if (revisionFile.exists())
698 if (revisionFile.open(QFile::ReadOnly | QFile::Text))
700 auto json = QJsonDocument::fromJson(revisionFile.readAll()).object();
701 result =
static_cast<qint64
>(json.value(
"timestamps").toObject().value(key).toDouble());
705 CLogMessage(
this).error(u
"Failed to open %1: %2")
706 << revisionFile.fileName() << revisionFile.errorString();
713 void CDataCacheRevision::pinValue(
const QString &key)
715 QMutexLocker lock(&m_mutex);
717 Q_ASSERT(!m_updateInProgress);
718 m_pinnedValues.insert(key);
721 void CDataCacheRevision::deferValue(
const QString &key)
723 QMutexLocker lock(&m_mutex);
725 Q_ASSERT(!m_updateInProgress);
726 m_deferredValues.insert(key);
729 void CDataCacheRevision::admitValue(
const QString &key)
731 QMutexLocker lock(&m_mutex);
733 m_admittedQueue.insert(key);
736 void CDataCacheRevision::sessionValue(
const QString &key)
738 QMutexLocker lock(&m_mutex);
740 Q_ASSERT(!m_updateInProgress);
741 m_sessionValues[key];
747 for (
auto it = timestamps.begin(); it != timestamps.end(); ++it) { result.insert(it.key(), it.value()); }
754 for (
auto it = timestamps.begin(); it != timestamps.end(); ++it)
756 result.insert(it.key(),
static_cast<qint64
>(it.value().toDouble()));
761 QJsonArray CDataCacheRevision::toJson(
const QSet<QString> &pins)
764 for (
auto it = pins.begin(); it != pins.end(); ++it) { result.push_back(*it); }
768 QSet<QString> CDataCacheRevision::fromJson(
const QJsonArray &pins)
770 QSet<QString> result;
771 for (
auto it = pins.begin(); it != pins.end(); ++it) { result.insert(it->toString()); }
778 for (
auto it = timestamps.begin(); it != timestamps.end(); ++it)
780 result.insert(it.key(), it.value().toString());
788 for (
auto it = session.begin(); it != session.end(); ++it)
790 result.insert(it.key(), QUuid(it.value().toString()));
795 void CDataCacheRevision::Session::updateSession()
797 CAtomicFile file(m_filename);
798 bool ok = file.open(QIODevice::ReadWrite | QFile::Text);
801 CLogMessage(
this).error(u
"Failed to open session file %1: %2") << m_filename << file.errorString();
804 auto json = QJsonDocument::fromJson(file.readAll()).object();
805 QUuid id(json.value(
"uuid").toString());
806 CSequence<CProcessInfo> apps;
807 auto status = apps.convertFromJsonNoThrow(json.value(
"apps").toObject(),
this,
808 QStringLiteral(
"Error in %1 apps object").arg(m_filename));
809 apps.removeIf([](
const CProcessInfo &pi) {
return !pi.exists(); });
811 if (apps.isEmpty()) {
id = CIdentifier().toUuid(); }
814 CProcessInfo currentProcess = CProcessInfo::currentProcess();
815 Q_ASSERT(currentProcess.exists());
816 apps.replaceOrAdd(currentProcess);
817 json.insert(
"apps", apps.toJson());
818 json.insert(
"uuid", m_uuid.toString());
819 if (file.seek(0) && file.resize(0) && file.write(QJsonDocument(json).toJson()))
821 if (!file.checkedClose())
823 static const QString advice =
824 QStringLiteral(
"If this error persists, try restarting your computer or delete the file manually.");
825 CLogMessage(
this).error(u
"Failed to replace %1: %2 (%3)")
826 << file.fileName() << file.errorString() << advice;
829 else { CLogMessage(
this).error(u
"Failed to write to %1: %2") << file.fileName() << file.errorString(); }
CDataCacheRevision & operator=(const CDataCacheRevision &)=delete
Non-copyable.
CDataCacheRevision(const QString &basename)
Construct the single instance of the revision metastate.
auto keys() const
Return a range of all keys (does not allocate a temporary container)
static const QString & cache()
Cache.
Value class used for signalling changed values in the cache.
QMap< QString, qint64 > toTimestampMap() const
Discard values and return as map of timestamps.
QString toTimestampMapString(const QStringList &keys) const
Return map of timestamps converted to string.
CVariantMap toVariantMap() const
Discard timestamps and return as variant map.
Map of { QString, CVariant } pairs.
Free functions in swift::misc.
void swap(Optional< T > &a, Optional< T > &b) noexcept(std::is_nothrow_swappable_v< T >)
Efficient swap for two Optional objects.
auto singleShot(int msec, QObject *target, F &&task)
Starts a single-shot timer which will call a task in the thread of the given object when it times out...
#define SWIFT_MISC_EXPORT
Export a class or function from the library.