/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce::midi_ci { class PropertyExchangeCache { public: PropertyExchangeCache() = default; struct OwningResult { explicit OwningResult (PropertyExchangeResult::Error e) : result (e) {} OwningResult (var header, std::vector body) : backingStorage (std::move (body)), result (header, backingStorage) {} OwningResult (OwningResult&&) noexcept = default; OwningResult& operator= (OwningResult&&) noexcept = default; JUCE_DECLARE_NON_COPYABLE (OwningResult) std::vector backingStorage; PropertyExchangeResult result; }; std::optional addChunk (Message::DynamicSizePropertyExchange chunk) { jassert (chunk.thisChunkNum == lastChunk + 1 || chunk.thisChunkNum == 0); lastChunk = chunk.thisChunkNum; headerStorage.reserve (headerStorage.size() + chunk.header.size()); std::transform (chunk.header.begin(), chunk.header.end(), std::back_inserter (headerStorage), [] (std::byte b) { return char (b); }); bodyStorage.insert (bodyStorage.end(), chunk.data.begin(), chunk.data.end()); if (chunk.thisChunkNum != 0 && chunk.thisChunkNum != chunk.totalNumChunks) return {}; const auto headerJson = JSON::parse (String (headerStorage.data(), headerStorage.size())); terminate(); const auto encodingString = headerJson.getProperty ("mutualEncoding", "ASCII").toString(); if (chunk.thisChunkNum != chunk.totalNumChunks) return std::optional { std::in_place, PropertyExchangeResult::Error::partial }; const int status = headerJson.getProperty ("status", 200); if (status == 343) return std::optional { std::in_place, PropertyExchangeResult::Error::tooManyTransactions }; return std::optional { std::in_place, headerJson, Encodings::decode (bodyStorage, EncodingUtils::toEncoding (encodingString.toRawUTF8()).value_or (Encoding::ascii)) }; } std::optional notify (Span header) { const auto headerJson = JSON::parse (String (reinterpret_cast (header.data()), header.size())); if (! headerJson.isObject()) return {}; const auto status = headerJson.getProperty ("status", {}); if (! status.isInt() || (int) status == 100) return {}; terminate(); return std::optional { std::in_place, PropertyExchangeResult::Error::notify }; } bool terminate() { return std::exchange (ongoing, false); } private: std::vector headerStorage; std::vector bodyStorage; uint16_t lastChunk = 0; bool ongoing = true; }; //============================================================================== class PropertyExchangeCacheArray { public: PropertyExchangeCacheArray() = default; Token64 primeCacheForRequestId (uint8_t id, std::function onDone) { jassert (id < caches.size()); ++lastKey; auto& entry = caches[id]; if (entry.has_value()) { // Trying to start a new message with the same id as another in-progress message jassertfalse; ids.erase (entry->key); } const auto& item = entry.emplace (id, std::move (onDone), Token64 { lastKey }); ids.emplace (item.key, id); return item.key; } bool terminate (Token64 key) { const auto iter = ids.find (key); // If the key isn't found, then the transaction must have completed already if (iter == ids.end()) return false; // We're about to terminate this transaction, so we don't need to retain this record auto index = iter->second; ids.erase (iter); auto& entry = caches[index]; // If the entry is null, something's gone wrong. The ids map should only contain elements for // non-null cache entries. if (! entry.has_value()) { jassertfalse; return false; } const auto result = entry->cache.terminate(); entry.reset(); return result; } void addChunk (RequestID b, const Message::DynamicSizePropertyExchange& chunk) { updateCache (b, [&] (PropertyExchangeCache& c) { return c.addChunk (chunk); }); } void notify (RequestID b, Span header) { updateCache (b, [&] (PropertyExchangeCache& c) { return c.notify (header); }); } std::optional getKeyForId (RequestID id) const { if (auto& c = caches[id.asInt()]) return c->key; return {}; } bool hasTransaction (RequestID id) const { return getKeyForId (id).has_value(); } std::optional getIdForKey (Token64 key) const { const auto iter = ids.find (key); return iter != ids.end() ? RequestID::create (iter->second) : std::nullopt; } auto countOngoingTransactions() const { jassert (ids.size() == (size_t) std::count_if (caches.begin(), caches.end(), [] (auto& c) { return c.has_value(); })); return (int) ids.size(); } auto getOngoingTransactions() const { jassert (ids.size() == (size_t) std::count_if (caches.begin(), caches.end(), [] (auto& c) { return c.has_value(); })); std::vector result (ids.size()); std::transform (ids.begin(), ids.end(), result.begin(), [] (const auto& p) { return Token64 { p.first }; }); return result; } std::optional findUnusedId (uint8_t maxSimultaneousTransactions) const { if (countOngoingTransactions() >= maxSimultaneousTransactions) return {}; return RequestID::create ((uint8_t) std::distance (caches.begin(), std::find (caches.begin(), caches.end(), std::nullopt))); } // Instances must stay at the same location to ensure that references captured in the // ErasedScopeGuard returned from primeCacheForRequestId do not dangle. JUCE_DECLARE_NON_COPYABLE (PropertyExchangeCacheArray) JUCE_DECLARE_NON_MOVEABLE (PropertyExchangeCacheArray) private: static constexpr auto numCaches = 128; class Transaction { public: Transaction (uint8_t i, std::function onSuccess, Token64 k) : onFinish (std::move (onSuccess)), key (k), id (i) {} PropertyExchangeCache cache; std::function onFinish; Token64 key{}; uint8_t id = 0; }; template void updateCache (RequestID b, WithCache&& withCache) { if (auto& entry = caches[b.asInt()]) { if (const auto result = withCache (entry->cache)) { const auto tmp = std::move (*entry); ids.erase (tmp.key); entry.reset(); NullCheckedInvocation::invoke (tmp.onFinish, result->result); } } } std::array, numCaches> caches; std::map ids; uint64_t lastKey = 0; }; //============================================================================== class InitiatorPropertyExchangeCache::Impl { public: std::optional primeCache (uint8_t maxSimultaneousRequests, std::function onDone) { const auto id = array.findUnusedId (maxSimultaneousRequests); return id.has_value() ? std::optional (array.primeCacheForRequestId (id->asInt(), std::move (onDone))) : std::nullopt; } bool terminate (Token64 token) { return array.terminate (token); } std::optional getTokenForRequestId (RequestID id) const { return array.getKeyForId (id); } std::optional getRequestIdForToken (Token64 token) const { return array.getIdForKey (token); } void addChunk (RequestID b, const Message::DynamicSizePropertyExchange& chunk) { array.addChunk (b, chunk); } void notify (RequestID b, Span header) { array.notify (b, header); } auto getOngoingTransactions() const { return array.getOngoingTransactions(); } private: PropertyExchangeCacheArray array; }; //============================================================================== InitiatorPropertyExchangeCache::InitiatorPropertyExchangeCache() : pimpl (std::make_unique()) {} InitiatorPropertyExchangeCache::InitiatorPropertyExchangeCache (InitiatorPropertyExchangeCache&&) noexcept = default; InitiatorPropertyExchangeCache& InitiatorPropertyExchangeCache::operator= (InitiatorPropertyExchangeCache&&) noexcept = default; InitiatorPropertyExchangeCache::~InitiatorPropertyExchangeCache() = default; std::optional InitiatorPropertyExchangeCache::primeCache (uint8_t maxSimultaneousTransactions, std::function onDone) { return pimpl->primeCache (maxSimultaneousTransactions, std::move (onDone)); } bool InitiatorPropertyExchangeCache::terminate (Token64 token) { return pimpl->terminate (token); } std::optional InitiatorPropertyExchangeCache::getTokenForRequestId (RequestID id) const { return pimpl->getTokenForRequestId (id); } std::optional InitiatorPropertyExchangeCache::getRequestIdForToken (Token64 token) const { return pimpl->getRequestIdForToken (token); } void InitiatorPropertyExchangeCache::addChunk (RequestID b, const Message::DynamicSizePropertyExchange& chunk) { pimpl->addChunk (b, chunk); } void InitiatorPropertyExchangeCache::notify (RequestID b, Span header) { pimpl->notify (b, header); } std::vector InitiatorPropertyExchangeCache::getOngoingTransactions() const { return pimpl->getOngoingTransactions(); } //============================================================================== class ResponderPropertyExchangeCache::Impl { public: void primeCache (uint8_t maxSimultaneousTransactions, std::function onDone, RequestID id) { if (array.hasTransaction (id)) return; if (array.countOngoingTransactions() >= maxSimultaneousTransactions) NullCheckedInvocation::invoke (onDone, PropertyExchangeResult { PropertyExchangeResult::Error::tooManyTransactions }); else array.primeCacheForRequestId (id.asInt(), std::move (onDone)); } void addChunk (RequestID b, const Message::DynamicSizePropertyExchange& chunk) { array.addChunk (b, chunk); } void notify (RequestID b, Span header) { array.notify (b, header); } int countOngoingTransactions() const { return array.countOngoingTransactions(); } private: PropertyExchangeCacheArray array; }; //============================================================================== ResponderPropertyExchangeCache::ResponderPropertyExchangeCache() : pimpl (std::make_unique()) {} ResponderPropertyExchangeCache::ResponderPropertyExchangeCache (ResponderPropertyExchangeCache&&) noexcept = default; ResponderPropertyExchangeCache& ResponderPropertyExchangeCache::operator= (ResponderPropertyExchangeCache&&) noexcept = default; ResponderPropertyExchangeCache::~ResponderPropertyExchangeCache() = default; void ResponderPropertyExchangeCache::primeCache (uint8_t maxSimultaneousTransactions, std::function onDone, RequestID id) { return pimpl->primeCache (maxSimultaneousTransactions, std::move (onDone), id); } void ResponderPropertyExchangeCache::addChunk (RequestID b, const Message::DynamicSizePropertyExchange& chunk) { pimpl->addChunk (b, chunk); } void ResponderPropertyExchangeCache::notify (RequestID b, Span header) { pimpl->notify (b, header); } int ResponderPropertyExchangeCache::countOngoingTransactions() const { return pimpl->countOngoingTransactions(); } } // namespace juce::midi_ci