mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
The old API only allowed cancelling property "get" inquiries and subscription updates. However, there are use-cases for cancelling other requests too. e.g. switching between views in a JUCE app might mean that it's no longer necessary to subscribe to a particular property. Cancelling subscriptions ends up being quite involved. Different handling is needed depending on whether the subscription is cancelled before or after the responder replies to the initial request. In addition, the responder may ask the initiator to retry a subscription begin request.
812 lines
31 KiB
C++
812 lines
31 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
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
|
|
{
|
|
|
|
struct RequestRetryQueueEntry
|
|
{
|
|
PropertySubscriptionHeader msg;
|
|
Token64 key{}; ///< A unique identifier for this message
|
|
bool inFlight = false; ///< True if the message has been sent and we're waiting for a reply, false otherwise
|
|
};
|
|
|
|
/*
|
|
A queue to store pending property exchange messages.
|
|
|
|
A property exchange message may fail to send because the initiator doesn't have enough vacant
|
|
property exchange IDs.
|
|
Similarly, if the responder doesn't have enough vacant IDs, then it may tell us to retry the
|
|
request.
|
|
|
|
We store messages that we're planning to send, and mark them as in-flight once we've attempted
|
|
to send them.
|
|
We always try to send the first not-in-flight message in the queue.
|
|
If the responder informs us that the message was actioned, or there was an unrecoverable error,
|
|
then we can remove the message from the queue. We can also remove the message if the user
|
|
decides that the message is no longer important.
|
|
Otherwise, if the message wasn't sent successfully, we leave the message at its current
|
|
position in the queue, and mark it as not-in-flight again.
|
|
*/
|
|
class RequestRetryQueue
|
|
{
|
|
using Entry = RequestRetryQueueEntry;
|
|
|
|
private:
|
|
auto getIter (Token64 k)
|
|
{
|
|
const auto iter = std::lower_bound (entries.begin(),
|
|
entries.end(),
|
|
(uint64_t) k,
|
|
[] (const Entry& e, uint64_t v) { return (uint64_t) e.key < v; });
|
|
return iter != entries.end() && iter->key == k ? iter : entries.end();
|
|
}
|
|
|
|
public:
|
|
/* Add a new message at the end of the queue, and return the entry for that message. */
|
|
Entry* add (PropertySubscriptionHeader msg)
|
|
{
|
|
const auto key = ++lastKey;
|
|
|
|
entries.push_back (Entry { std::move (msg), Token64 { key }, false });
|
|
return &entries.back();
|
|
}
|
|
|
|
/* Erase the entry for a given key. */
|
|
std::optional<Entry> erase (Token64 k)
|
|
{
|
|
const auto iter = getIter (k);
|
|
|
|
if (iter == entries.end())
|
|
return {};
|
|
|
|
auto result = std::move (*iter);
|
|
entries.erase (iter);
|
|
return result;
|
|
}
|
|
|
|
/* Find the next entry that should be sent, and return it after marking it as in-flight. */
|
|
const Entry* markNextInFlight()
|
|
{
|
|
const auto iter = std::find_if (entries.begin(), entries.end(), [] (const Entry& e) { return ! e.inFlight; });
|
|
|
|
if (iter == entries.end())
|
|
return nullptr;
|
|
|
|
iter->inFlight = true;
|
|
return &*iter;
|
|
}
|
|
|
|
void markNotInFlight (Token64 k)
|
|
{
|
|
const auto iter = getIter (k);
|
|
|
|
if (iter != entries.end())
|
|
iter->inFlight = false;
|
|
}
|
|
|
|
private:
|
|
std::vector<Entry> entries;
|
|
uint64_t lastKey = 0;
|
|
};
|
|
|
|
/**
|
|
Info about a particular subscription.
|
|
|
|
You can think of this as a subscription agreement as identified by a subscribeId, but this
|
|
also holds state that is necessary to negotiate the subscribeId.
|
|
*/
|
|
struct SubscriptionState
|
|
{
|
|
// If we're waiting to send this subscription request, this is monostate
|
|
// If the request has been sent, but we haven't received a reply, this is the id of the request
|
|
// If the subscription started successfully, this is the subscribeId for the subscription
|
|
std::variant<std::monostate, Token64, String> state;
|
|
String resource;
|
|
};
|
|
|
|
/**
|
|
Info about all the subscriptions requested of a particular device/MUID.
|
|
This keeps track of the order in which subscription requests are made, so that requests can
|
|
be re-tried in order if the initial sending of a request fails.
|
|
*/
|
|
class DeviceSubscriptionStates
|
|
{
|
|
public:
|
|
Token64 postToQueue (const PropertySubscriptionHeader& header)
|
|
{
|
|
return queue.add (header)->key;
|
|
}
|
|
|
|
Token64 beginSubscription (const PropertySubscriptionHeader& header)
|
|
{
|
|
jassert (header.command == PropertySubscriptionCommand::start);
|
|
|
|
auto headerCopy = header;
|
|
headerCopy.command = PropertySubscriptionCommand::start;
|
|
|
|
const auto key = postToQueue (headerCopy);
|
|
stateForSubscription[key].resource = headerCopy.resource;
|
|
|
|
return key;
|
|
}
|
|
|
|
std::optional<SubscriptionState> endSubscription (Token64 key)
|
|
{
|
|
queue.erase (key);
|
|
|
|
const auto iter = stateForSubscription.find (key);
|
|
|
|
if (iter == stateForSubscription.end())
|
|
return {};
|
|
|
|
auto subInfo = iter->second;
|
|
stateForSubscription.erase (iter);
|
|
|
|
return { std::move (subInfo) };
|
|
}
|
|
|
|
std::vector<Token64> endSubscription (String subscribeId)
|
|
{
|
|
std::vector<Token64> ended;
|
|
|
|
for (auto it = stateForSubscription.begin(); it != stateForSubscription.end();)
|
|
{
|
|
if (const auto* id = std::get_if<String> (&it->second.state))
|
|
{
|
|
if (*id == subscribeId)
|
|
{
|
|
ended.push_back (it->first);
|
|
queue.erase (it->first);
|
|
it = stateForSubscription.erase (it);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
++it;
|
|
}
|
|
|
|
return ended;
|
|
}
|
|
|
|
void endAll()
|
|
{
|
|
for (auto& item : stateForSubscription)
|
|
queue.erase (item.first);
|
|
|
|
stateForSubscription.clear();
|
|
}
|
|
|
|
void resetKey (Token64 key)
|
|
{
|
|
const auto iter = stateForSubscription.find (key);
|
|
|
|
if (iter != stateForSubscription.end())
|
|
iter->second.state = std::monostate{};
|
|
|
|
queue.markNotInFlight (key);
|
|
}
|
|
|
|
void setRequestIdForKey (Token64 key, Token64 request)
|
|
{
|
|
const auto iter = stateForSubscription.find (key);
|
|
|
|
if (iter != stateForSubscription.end())
|
|
iter->second.state = request;
|
|
}
|
|
|
|
void setSubscribeIdForKey (Token64 key, String subscribeId)
|
|
{
|
|
const auto iter = stateForSubscription.find (key);
|
|
|
|
if (iter != stateForSubscription.end())
|
|
iter->second.state = subscribeId;
|
|
|
|
queue.erase (key);
|
|
}
|
|
|
|
auto* markNextInFlight()
|
|
{
|
|
return queue.markNextInFlight();
|
|
}
|
|
|
|
std::optional<SubscriptionState> getInfoForSubscriptionKey (Token64 key) const
|
|
{
|
|
const auto iter = stateForSubscription.find (key);
|
|
|
|
if (iter != stateForSubscription.end())
|
|
return iter->second;
|
|
|
|
return {};
|
|
}
|
|
|
|
auto begin() const { return stateForSubscription.begin(); }
|
|
auto end() const { return stateForSubscription.end(); }
|
|
|
|
private:
|
|
RequestRetryQueue queue;
|
|
std::map<Token64, SubscriptionState> stateForSubscription;
|
|
};
|
|
|
|
class SubscriptionManager::Impl : public std::enable_shared_from_this<Impl>,
|
|
private DeviceListener
|
|
{
|
|
public:
|
|
explicit Impl (SubscriptionManagerDelegate& d)
|
|
: delegate (d) {}
|
|
|
|
SubscriptionKey beginSubscription (MUID m, const PropertySubscriptionHeader& header)
|
|
{
|
|
const auto key = infoForMuid[m].beginSubscription (header);
|
|
sendPendingMessages();
|
|
return SubscriptionKey { m, key };
|
|
}
|
|
|
|
void endSubscription (SubscriptionKey key)
|
|
{
|
|
const auto iter = infoForMuid.find (key.getMuid());
|
|
|
|
if (iter == infoForMuid.end())
|
|
return;
|
|
|
|
const auto ended = iter->second.endSubscription (key.getKey());
|
|
|
|
if (! ended.has_value())
|
|
return;
|
|
|
|
if (auto* request = std::get_if<Token64> (&ended->state))
|
|
{
|
|
delegate.abortPropertyRequest ({ key.getMuid(), *request });
|
|
}
|
|
else if (auto* subscribeId = std::get_if<String> (&ended->state))
|
|
{
|
|
PropertySubscriptionHeader header;
|
|
header.command = PropertySubscriptionCommand::end;
|
|
header.subscribeId = *subscribeId;
|
|
iter->second.postToQueue (header);
|
|
sendPendingMessages();
|
|
}
|
|
}
|
|
|
|
void endSubscriptionFromResponder (MUID m, String sub)
|
|
{
|
|
const auto iter = infoForMuid.find (m);
|
|
|
|
if (iter != infoForMuid.end())
|
|
for (const auto& ended : iter->second.endSubscription (sub))
|
|
delegate.propertySubscriptionChanged ({ m, ended }, std::nullopt);
|
|
}
|
|
|
|
void endSubscriptionsFromResponder (MUID m)
|
|
{
|
|
const auto iter = infoForMuid.find (m);
|
|
|
|
if (iter == infoForMuid.end())
|
|
return;
|
|
|
|
std::vector<Token64> tokens;
|
|
std::transform (iter->second.begin(),
|
|
iter->second.end(),
|
|
std::back_inserter (tokens),
|
|
[] (const auto& p) { return p.first; });
|
|
|
|
iter->second.endAll();
|
|
|
|
for (const auto& ended : tokens)
|
|
delegate.propertySubscriptionChanged ({ m, ended }, std::nullopt);
|
|
}
|
|
|
|
std::vector<SubscriptionKey> getOngoingSubscriptions() const
|
|
{
|
|
std::vector<SubscriptionKey> result;
|
|
|
|
for (const auto& pair : infoForMuid)
|
|
for (const auto& info : pair.second)
|
|
result.emplace_back (pair.first, info.first);
|
|
|
|
return result;
|
|
}
|
|
|
|
std::optional<SubscriptionState> getInfoForSubscriptionKey (SubscriptionKey key) const
|
|
{
|
|
const auto iter = infoForMuid.find (key.getMuid());
|
|
|
|
if (iter != infoForMuid.end())
|
|
return iter->second.getInfoForSubscriptionKey (key.getKey());
|
|
|
|
return {};
|
|
}
|
|
|
|
bool sendPendingMessages()
|
|
{
|
|
// Note: not using any_of here because we don't want the early-exit behaviour
|
|
bool result = true;
|
|
|
|
for (auto& pair : infoForMuid)
|
|
result &= sendPendingMessages (pair.first, pair.second);
|
|
|
|
return result;
|
|
}
|
|
|
|
private:
|
|
void handleReply (SubscriptionKey subscriptionKey, PropertySubscriptionCommand command, const PropertyExchangeResult& r)
|
|
{
|
|
const auto iter = infoForMuid.find (subscriptionKey.getMuid());
|
|
|
|
if (iter == infoForMuid.end())
|
|
return;
|
|
|
|
auto& second = iter->second;
|
|
|
|
if (const auto error = r.getError())
|
|
{
|
|
// If the responder requested a retry, keep the message in the queue so that
|
|
// it can be re-sent
|
|
if (*error == PropertyExchangeResult::Error::tooManyTransactions)
|
|
{
|
|
second.resetKey (subscriptionKey.getKey());
|
|
return;
|
|
}
|
|
|
|
// We tried to begin or end a subscription, but the responder said no!
|
|
// If the responder declined to start a subscription, we can just
|
|
// mark the subscription as ended.
|
|
// If the responder declined to end a subscription, that's a bit trickier.
|
|
// Hopefully this won't happen in practice, because all the options to resolve are pretty bad:
|
|
// - One option is to ignore the failure. The remote device can carry on sending us updates.
|
|
// This might be a bit dangerous if we repeatedly subscribe and then fail to unsubscribe, as this
|
|
// would result in lots of redundant subscription messages that could clog the connection.
|
|
// - Another option is to store the subscription-end request and to attempt to send it again later.
|
|
// This also has the potential to clog up the connection, depending on how frequently we attempt
|
|
// to re-send failed messages. Given that unsubscribing has already failed once, there's no
|
|
// guarantee that any future attempts will succeed, so we might end up in a loop, sending the
|
|
// same message over and over.
|
|
// On balance, I think the former option is best for now. If this ends up being an issue in
|
|
// practice, perhaps we could add a mechanism to do exponential back-off, but that would
|
|
// add complexity that isn't necessarily required.
|
|
jassert (*error != PropertyExchangeResult::Error::notify);
|
|
|
|
// If we failed to begin a subscription, then the subscription never started,
|
|
// and we should remove it from the set of ongoing subscriptions.
|
|
second.endSubscription (subscriptionKey.getKey());
|
|
|
|
// We only need to alert the delegate if the subscription failed to start.
|
|
// If the subscription fails to end, we'll treat the subscription as ended anyway.
|
|
if (command == PropertySubscriptionCommand::start)
|
|
delegate.propertySubscriptionChanged (subscriptionKey, std::nullopt);
|
|
|
|
return;
|
|
}
|
|
|
|
if (command == PropertySubscriptionCommand::start)
|
|
{
|
|
second.setSubscribeIdForKey (subscriptionKey.getKey(), r.getHeaderAsSubscriptionHeader().subscribeId);
|
|
delegate.propertySubscriptionChanged (subscriptionKey, r.getHeaderAsSubscriptionHeader().subscribeId);
|
|
}
|
|
}
|
|
|
|
bool sendPendingMessages (MUID m, DeviceSubscriptionStates& info)
|
|
{
|
|
while (auto* entry = info.markNextInFlight())
|
|
{
|
|
const auto requestKind = entry->msg.command;
|
|
const SubscriptionKey subscriptionKey { m, entry->key };
|
|
|
|
auto cb = [weak = weak_from_this(), requestKind, subscriptionKey] (const PropertyExchangeResult& r)
|
|
{
|
|
if (const auto locked = weak.lock())
|
|
locked->handleReply (subscriptionKey, requestKind, r);
|
|
};
|
|
|
|
if (const auto request = delegate.sendPropertySubscribe (m, entry->msg, std::move (cb)))
|
|
{
|
|
if (entry->msg.command == PropertySubscriptionCommand::start)
|
|
info.setRequestIdForKey (entry->key, request->getKey());
|
|
}
|
|
else
|
|
{
|
|
// Couldn't find a valid ID to use, so we must have exhausted all message slots.
|
|
// There's no point trying to send the rest of the messages that are queued for this
|
|
// MUID, so give up. It's probably a good idea to try again in a bit.
|
|
info.resetKey (entry->key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
SubscriptionManagerDelegate& delegate;
|
|
std::map<MUID, DeviceSubscriptionStates> infoForMuid;
|
|
};
|
|
|
|
//==============================================================================
|
|
SubscriptionManager::SubscriptionManager (SubscriptionManagerDelegate& delegate)
|
|
: pimpl (std::make_shared<Impl> (delegate)) {}
|
|
|
|
SubscriptionKey SubscriptionManager::beginSubscription (MUID m, const PropertySubscriptionHeader& header)
|
|
{
|
|
return pimpl->beginSubscription (m, header);
|
|
}
|
|
|
|
void SubscriptionManager::endSubscription (SubscriptionKey key)
|
|
{
|
|
pimpl->endSubscription (key);
|
|
}
|
|
|
|
void SubscriptionManager::endSubscriptionFromResponder (MUID m, String sub)
|
|
{
|
|
pimpl->endSubscriptionFromResponder (m, sub);
|
|
}
|
|
|
|
void SubscriptionManager::endSubscriptionsFromResponder (MUID m)
|
|
{
|
|
pimpl->endSubscriptionsFromResponder (m);
|
|
}
|
|
|
|
std::vector<SubscriptionKey> SubscriptionManager::getOngoingSubscriptions() const
|
|
{
|
|
return pimpl->getOngoingSubscriptions();
|
|
}
|
|
|
|
std::optional<String> SubscriptionManager::getSubscribeIdForKey (SubscriptionKey key) const
|
|
{
|
|
if (const auto info = pimpl->getInfoForSubscriptionKey (key))
|
|
if (const auto* subscribeId = std::get_if<String> (&info->state))
|
|
return *subscribeId;
|
|
|
|
return {};
|
|
}
|
|
|
|
std::optional<String> SubscriptionManager::getResourceForKey (SubscriptionKey key) const
|
|
{
|
|
if (const auto info = pimpl->getInfoForSubscriptionKey (key))
|
|
return info->resource;
|
|
|
|
return {};
|
|
}
|
|
|
|
bool SubscriptionManager::sendPendingMessages()
|
|
{
|
|
return pimpl->sendPendingMessages();
|
|
}
|
|
|
|
//==============================================================================
|
|
//==============================================================================
|
|
#if JUCE_UNIT_TESTS
|
|
|
|
class SubscriptionTests : public UnitTest
|
|
{
|
|
public:
|
|
SubscriptionTests() : UnitTest ("Subscription", UnitTestCategories::midi) {}
|
|
|
|
void runTest() override
|
|
{
|
|
auto random = getRandom();
|
|
|
|
class Delegate : public SubscriptionManagerDelegate
|
|
{
|
|
public:
|
|
std::optional<RequestKey> sendPropertySubscribe (MUID m,
|
|
const PropertySubscriptionHeader&,
|
|
std::function<void (const PropertyExchangeResult&)> cb) override
|
|
{
|
|
++sendCount;
|
|
|
|
if (! sendShouldSucceed)
|
|
return {};
|
|
|
|
const RequestKey key { m, Token64 { ++lastKey } };
|
|
callbacks[key] = std::move (cb);
|
|
|
|
return key;
|
|
}
|
|
|
|
void abortPropertyRequest (RequestKey k) override
|
|
{
|
|
++abortCount;
|
|
callbacks.erase (k);
|
|
}
|
|
|
|
void propertySubscriptionChanged (SubscriptionKey, const std::optional<String>&) override
|
|
{
|
|
subChanged = true;
|
|
}
|
|
|
|
void setSendShouldSucceed (bool b) { sendShouldSucceed = b; }
|
|
|
|
void sendResult (RequestKey key, const PropertyExchangeResult& r)
|
|
{
|
|
const auto iter = callbacks.find (key);
|
|
|
|
if (iter != callbacks.end())
|
|
NullCheckedInvocation::invoke (iter->second, r);
|
|
|
|
callbacks.erase (key);
|
|
}
|
|
|
|
std::vector<RequestKey> getOngoingRequests() const
|
|
{
|
|
std::vector<RequestKey> result;
|
|
result.reserve (callbacks.size());
|
|
std::transform (callbacks.begin(), callbacks.end(), std::back_inserter (result), [] (const auto& p) { return p.first; });
|
|
return result;
|
|
}
|
|
|
|
uint64_t getAndClearSendCount() { return std::exchange (sendCount, 0); }
|
|
uint64_t getAndClearAbortCount() { return std::exchange (abortCount, 0); }
|
|
bool getAndClearSubChanged() { return std::exchange (subChanged, false); }
|
|
|
|
private:
|
|
std::map<RequestKey, std::function<void (const PropertyExchangeResult&)>> callbacks;
|
|
uint64_t sendCount = 0, abortCount = 0;
|
|
uint64_t lastKey = 0;
|
|
bool sendShouldSucceed = true, subChanged = false;
|
|
};
|
|
|
|
Delegate delegate;
|
|
SubscriptionManager manager { delegate };
|
|
|
|
const auto inquiryMUID = MUID::makeRandom (random);
|
|
|
|
beginTest ("Beginning a subscription and ending it before the remote device replies aborts the request");
|
|
{
|
|
PropertySubscriptionHeader header;
|
|
header.command = PropertySubscriptionCommand::start;
|
|
header.resource = "X-CustomProp";
|
|
|
|
const auto a = manager.beginSubscription (inquiryMUID, header);
|
|
|
|
expect (manager.getOngoingSubscriptions() == std::vector { a });
|
|
expect (delegate.getAndClearSendCount() == 1);
|
|
|
|
// Sending a subscription request uses a request slot
|
|
expect (delegate.getOngoingRequests().size() == 1);
|
|
const auto request = delegate.getOngoingRequests().back();
|
|
|
|
// subscription id is empty until the responder confirms the subscription
|
|
expect (manager.getResourceForKey (a) == header.resource);
|
|
|
|
manager.endSubscription (a);
|
|
|
|
expect (delegate.getOngoingRequests().empty());
|
|
expect (delegate.getAndClearAbortCount() == 1);
|
|
expect (manager.getOngoingSubscriptions().empty());
|
|
|
|
const auto successHeader = []
|
|
{
|
|
auto ptr = std::make_unique<DynamicObject>();
|
|
ptr->setProperty ("status", 200);
|
|
ptr->setProperty ("subscribeId", "anId");
|
|
return var { ptr.release() };
|
|
}();
|
|
|
|
delegate.sendResult (request, PropertyExchangeResult { successHeader, {} });
|
|
|
|
// Already ended, the confirmation shouldn't do anything
|
|
expect (! delegate.getAndClearSubChanged());
|
|
expect (delegate.getOngoingRequests().empty());
|
|
expect (delegate.getAndClearSendCount() == 0);
|
|
expect (delegate.getAndClearAbortCount() == 0);
|
|
|
|
// There shouldn't be any queued messages.
|
|
expect (manager.sendPendingMessages());
|
|
expect (! delegate.getAndClearSubChanged());
|
|
expect (delegate.getOngoingRequests().empty());
|
|
expect (delegate.getAndClearSendCount() == 0);
|
|
expect (delegate.getAndClearAbortCount() == 0);
|
|
}
|
|
|
|
beginTest ("Starting a new subscription while the device is waiting for a previous subscription to be confirmed queues further requests");
|
|
{
|
|
PropertySubscriptionHeader header;
|
|
header.command = PropertySubscriptionCommand::start;
|
|
header.resource = "X-CustomProp";
|
|
|
|
delegate.setSendShouldSucceed (false);
|
|
const auto a = manager.beginSubscription (inquiryMUID, header);
|
|
expect (delegate.getAndClearSendCount() == 1);
|
|
|
|
expect (! manager.sendPendingMessages());
|
|
expect (delegate.getAndClearSendCount() == 1);
|
|
|
|
delegate.setSendShouldSucceed (true);
|
|
expect (manager.sendPendingMessages());
|
|
expect (delegate.getAndClearSendCount() == 1);
|
|
|
|
delegate.setSendShouldSucceed (false);
|
|
const auto b = manager.beginSubscription (inquiryMUID, header);
|
|
const auto c = manager.beginSubscription (inquiryMUID, header);
|
|
|
|
expect (manager.getOngoingSubscriptions() == std::vector { a, b, c });
|
|
expect (delegate.getOngoingRequests().size() == 1);
|
|
|
|
// subscription id is empty until the responder confirms the subscription
|
|
expect (manager.getResourceForKey (a) == header.resource);
|
|
expect (manager.getResourceForKey (b) == header.resource);
|
|
expect (manager.getResourceForKey (c) == header.resource);
|
|
|
|
expect (delegate.getAndClearSendCount() == 2);
|
|
expect (delegate.getAndClearAbortCount() == 0);
|
|
|
|
delegate.setSendShouldSucceed (true);
|
|
|
|
// The device has sent a subscription start for a, but not for c,
|
|
// so it should send a notify to end subscription a, but shouldn't emit any
|
|
// messages related to subscription c.
|
|
manager.endSubscription (a);
|
|
manager.endSubscription (c);
|
|
|
|
expect (manager.getOngoingSubscriptions() == std::vector { b });
|
|
|
|
expect (delegate.getOngoingRequests().empty());
|
|
expect (delegate.getAndClearSendCount() == 0);
|
|
expect (delegate.getAndClearAbortCount() == 1);
|
|
|
|
// There should still be requests related to subscription b pending
|
|
expect (manager.sendPendingMessages());
|
|
expect (delegate.getOngoingRequests().size() == 1);
|
|
expect (delegate.getAndClearSendCount() == 1);
|
|
expect (delegate.getAndClearAbortCount() == 0);
|
|
|
|
// Now, we should send a terminate request for subscription b
|
|
manager.endSubscription (b);
|
|
|
|
expect (delegate.getOngoingRequests().empty());
|
|
expect (delegate.getAndClearSendCount() == 0);
|
|
expect (delegate.getAndClearAbortCount() == 1);
|
|
|
|
// The manager never received any replies, so it shouldn't have notified listeners about
|
|
// changed subscriptions
|
|
expect (! delegate.getAndClearSubChanged());
|
|
}
|
|
|
|
beginTest ("If the device receives a retry or notify in response to a subscription start request, the subscription is retried or terminated as necessary");
|
|
{
|
|
PropertySubscriptionHeader header;
|
|
header.command = PropertySubscriptionCommand::start;
|
|
header.resource = "X-CustomProp";
|
|
|
|
const auto a = manager.beginSubscription (inquiryMUID, header);
|
|
|
|
expect (manager.getOngoingSubscriptions() == std::vector { a });
|
|
expect (delegate.getOngoingRequests().size() == 1);
|
|
expect (delegate.getAndClearSendCount() == 1);
|
|
expect (delegate.getAndClearAbortCount() == 0);
|
|
|
|
delegate.sendResult (delegate.getOngoingRequests().back(), PropertyExchangeResult { PropertyExchangeResult::Error::tooManyTransactions });
|
|
|
|
// The subscription is still active from the perspective of the manager, but the
|
|
// first request is over and should be retried
|
|
expect (manager.getOngoingSubscriptions() == std::vector { a });
|
|
expect (! delegate.getAndClearSubChanged());
|
|
expect (delegate.getOngoingRequests().empty());
|
|
expect (delegate.getAndClearSendCount() == 0);
|
|
expect (delegate.getAndClearAbortCount() == 0);
|
|
|
|
expect (manager.sendPendingMessages());
|
|
|
|
expect (manager.getOngoingSubscriptions() == std::vector { a });
|
|
expect (delegate.getOngoingRequests().size() == 1);
|
|
expect (delegate.getAndClearSendCount() == 1);
|
|
expect (delegate.getAndClearAbortCount() == 0);
|
|
|
|
delegate.sendResult (delegate.getOngoingRequests().back(), PropertyExchangeResult { PropertyExchangeResult::Error::notify });
|
|
|
|
// The request was terminated by the responder, so the delegate should get a sub-changed message
|
|
expect (delegate.getAndClearSubChanged());
|
|
expect (manager.getOngoingSubscriptions().empty());
|
|
expect (delegate.getOngoingRequests().empty());
|
|
expect (delegate.getAndClearSendCount() == 0);
|
|
expect (delegate.getAndClearAbortCount() == 0);
|
|
|
|
expect (manager.sendPendingMessages());
|
|
expect (! delegate.getAndClearSubChanged());
|
|
expect (manager.getOngoingSubscriptions().empty());
|
|
expect (delegate.getOngoingRequests().empty());
|
|
expect (delegate.getAndClearSendCount() == 0);
|
|
expect (delegate.getAndClearAbortCount() == 0);
|
|
}
|
|
|
|
beginTest ("If the device receives a retry or notify in response to a subscription end request, the subscription is retried as necessary");
|
|
{
|
|
PropertySubscriptionHeader header;
|
|
header.command = PropertySubscriptionCommand::start;
|
|
header.resource = "X-CustomProp";
|
|
|
|
const auto a = manager.beginSubscription (inquiryMUID, header);
|
|
|
|
expect (manager.getOngoingSubscriptions() == std::vector { a });
|
|
expect (manager.getResourceForKey (a) == header.resource);
|
|
expect (! manager.getSubscribeIdForKey (a).has_value());
|
|
|
|
const auto subscriptionResponseHeader = []
|
|
{
|
|
auto ptr = std::make_unique<DynamicObject>();
|
|
ptr->setProperty ("status", 200);
|
|
ptr->setProperty ("subscribeId", "newId");
|
|
return ptr.release();
|
|
}();
|
|
|
|
// Accept the subscription
|
|
delegate.sendResult (delegate.getOngoingRequests().back(), PropertyExchangeResult { subscriptionResponseHeader, {} });
|
|
|
|
// The subscription is still active from the perspective of the device, but the
|
|
// request is over and should be retried
|
|
expect (manager.getOngoingSubscriptions() == std::vector { a });
|
|
expect (delegate.getAndClearSubChanged());
|
|
// Now that the subscription was accepted, the subscription id should be non-empty
|
|
expect (manager.getResourceForKey (a) == header.resource);
|
|
expect (manager.getSubscribeIdForKey (a) == "newId");
|
|
expect (delegate.getOngoingRequests().empty());
|
|
expect (delegate.getAndClearSendCount() == 1);
|
|
expect (delegate.getAndClearAbortCount() == 0);
|
|
|
|
manager.endSubscription (a);
|
|
|
|
expect (manager.getOngoingSubscriptions().empty());
|
|
expect (! delegate.getAndClearSubChanged());
|
|
expect (delegate.getOngoingRequests().size() == 1);
|
|
expect (delegate.getAndClearSendCount() == 1);
|
|
expect (delegate.getAndClearAbortCount() == 0);
|
|
|
|
// The responder is busy, can't process the subscription end
|
|
delegate.sendResult (delegate.getOngoingRequests().back(), PropertyExchangeResult { PropertyExchangeResult::Error::tooManyTransactions });
|
|
|
|
expect (manager.getOngoingSubscriptions().empty());
|
|
expect (delegate.getOngoingRequests().empty());
|
|
expect (delegate.getAndClearSendCount() == 0);
|
|
expect (delegate.getAndClearAbortCount() == 0);
|
|
|
|
expect (manager.sendPendingMessages());
|
|
expect (manager.getOngoingSubscriptions().empty());
|
|
expect (delegate.getOngoingRequests().size() == 1);
|
|
expect (delegate.getAndClearSendCount() == 1);
|
|
expect (delegate.getAndClearAbortCount() == 0);
|
|
|
|
// The responder told us to immediately terminate our request to end the subscription!
|
|
// It's unclear how this should behave, so we'll just ignore the failure and assume
|
|
// the subscription is really over.
|
|
delegate.sendResult (delegate.getOngoingRequests().back(), PropertyExchangeResult { PropertyExchangeResult::Error::notify });
|
|
|
|
expect (manager.getOngoingSubscriptions().empty());
|
|
expect (delegate.getOngoingRequests().empty());
|
|
expect (delegate.getAndClearSendCount() == 0);
|
|
expect (delegate.getAndClearAbortCount() == 0);
|
|
|
|
expect (manager.sendPendingMessages());
|
|
expect (delegate.getAndClearSendCount() == 0);
|
|
expect (delegate.getAndClearAbortCount() == 0);
|
|
|
|
expect (! delegate.getAndClearSubChanged());
|
|
}
|
|
}
|
|
};
|
|
|
|
static SubscriptionTests subscriptionTests;
|
|
|
|
#endif
|
|
|
|
} // namespace juce::midi_ci
|