The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

813 lines
31KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. namespace juce::midi_ci
  19. {
  20. struct RequestRetryQueueEntry
  21. {
  22. PropertySubscriptionHeader msg;
  23. Token64 key{}; ///< A unique identifier for this message
  24. bool inFlight = false; ///< True if the message has been sent and we're waiting for a reply, false otherwise
  25. };
  26. /*
  27. A queue to store pending property exchange messages.
  28. A property exchange message may fail to send because the initiator doesn't have enough vacant
  29. property exchange IDs.
  30. Similarly, if the responder doesn't have enough vacant IDs, then it may tell us to retry the
  31. request.
  32. We store messages that we're planning to send, and mark them as in-flight once we've attempted
  33. to send them.
  34. We always try to send the first not-in-flight message in the queue.
  35. If the responder informs us that the message was actioned, or there was an unrecoverable error,
  36. then we can remove the message from the queue. We can also remove the message if the user
  37. decides that the message is no longer important.
  38. Otherwise, if the message wasn't sent successfully, we leave the message at its current
  39. position in the queue, and mark it as not-in-flight again.
  40. */
  41. class RequestRetryQueue
  42. {
  43. using Entry = RequestRetryQueueEntry;
  44. private:
  45. auto getIter (Token64 k)
  46. {
  47. const auto iter = std::lower_bound (entries.begin(),
  48. entries.end(),
  49. (uint64_t) k,
  50. [] (const Entry& e, uint64_t v) { return (uint64_t) e.key < v; });
  51. return iter != entries.end() && iter->key == k ? iter : entries.end();
  52. }
  53. public:
  54. /* Add a new message at the end of the queue, and return the entry for that message. */
  55. Entry* add (PropertySubscriptionHeader msg)
  56. {
  57. const auto key = ++lastKey;
  58. entries.push_back (Entry { std::move (msg), Token64 { key }, false });
  59. return &entries.back();
  60. }
  61. /* Erase the entry for a given key. */
  62. std::optional<Entry> erase (Token64 k)
  63. {
  64. const auto iter = getIter (k);
  65. if (iter == entries.end())
  66. return {};
  67. auto result = std::move (*iter);
  68. entries.erase (iter);
  69. return result;
  70. }
  71. /* Find the next entry that should be sent, and return it after marking it as in-flight. */
  72. const Entry* markNextInFlight()
  73. {
  74. const auto iter = std::find_if (entries.begin(), entries.end(), [] (const Entry& e) { return ! e.inFlight; });
  75. if (iter == entries.end())
  76. return nullptr;
  77. iter->inFlight = true;
  78. return &*iter;
  79. }
  80. void markNotInFlight (Token64 k)
  81. {
  82. const auto iter = getIter (k);
  83. if (iter != entries.end())
  84. iter->inFlight = false;
  85. }
  86. private:
  87. std::vector<Entry> entries;
  88. uint64_t lastKey = 0;
  89. };
  90. /**
  91. Info about a particular subscription.
  92. You can think of this as a subscription agreement as identified by a subscribeId, but this
  93. also holds state that is necessary to negotiate the subscribeId.
  94. */
  95. struct SubscriptionState
  96. {
  97. // If we're waiting to send this subscription request, this is monostate
  98. // If the request has been sent, but we haven't received a reply, this is the id of the request
  99. // If the subscription started successfully, this is the subscribeId for the subscription
  100. std::variant<std::monostate, Token64, String> state;
  101. String resource;
  102. };
  103. /**
  104. Info about all the subscriptions requested of a particular device/MUID.
  105. This keeps track of the order in which subscription requests are made, so that requests can
  106. be re-tried in order if the initial sending of a request fails.
  107. */
  108. class DeviceSubscriptionStates
  109. {
  110. public:
  111. Token64 postToQueue (const PropertySubscriptionHeader& header)
  112. {
  113. return queue.add (header)->key;
  114. }
  115. Token64 beginSubscription (const PropertySubscriptionHeader& header)
  116. {
  117. jassert (header.command == PropertySubscriptionCommand::start);
  118. auto headerCopy = header;
  119. headerCopy.command = PropertySubscriptionCommand::start;
  120. const auto key = postToQueue (headerCopy);
  121. stateForSubscription[key].resource = headerCopy.resource;
  122. return key;
  123. }
  124. std::optional<SubscriptionState> endSubscription (Token64 key)
  125. {
  126. queue.erase (key);
  127. const auto iter = stateForSubscription.find (key);
  128. if (iter == stateForSubscription.end())
  129. return {};
  130. auto subInfo = iter->second;
  131. stateForSubscription.erase (iter);
  132. return { std::move (subInfo) };
  133. }
  134. std::vector<Token64> endSubscription (String subscribeId)
  135. {
  136. std::vector<Token64> ended;
  137. for (auto it = stateForSubscription.begin(); it != stateForSubscription.end();)
  138. {
  139. if (const auto* id = std::get_if<String> (&it->second.state))
  140. {
  141. if (*id == subscribeId)
  142. {
  143. ended.push_back (it->first);
  144. queue.erase (it->first);
  145. it = stateForSubscription.erase (it);
  146. continue;
  147. }
  148. }
  149. ++it;
  150. }
  151. return ended;
  152. }
  153. void endAll()
  154. {
  155. for (auto& item : stateForSubscription)
  156. queue.erase (item.first);
  157. stateForSubscription.clear();
  158. }
  159. void resetKey (Token64 key)
  160. {
  161. const auto iter = stateForSubscription.find (key);
  162. if (iter != stateForSubscription.end())
  163. iter->second.state = std::monostate{};
  164. queue.markNotInFlight (key);
  165. }
  166. void setRequestIdForKey (Token64 key, Token64 request)
  167. {
  168. const auto iter = stateForSubscription.find (key);
  169. if (iter != stateForSubscription.end())
  170. iter->second.state = request;
  171. }
  172. void setSubscribeIdForKey (Token64 key, String subscribeId)
  173. {
  174. const auto iter = stateForSubscription.find (key);
  175. if (iter != stateForSubscription.end())
  176. iter->second.state = subscribeId;
  177. queue.erase (key);
  178. }
  179. auto* markNextInFlight()
  180. {
  181. return queue.markNextInFlight();
  182. }
  183. std::optional<SubscriptionState> getInfoForSubscriptionKey (Token64 key) const
  184. {
  185. const auto iter = stateForSubscription.find (key);
  186. if (iter != stateForSubscription.end())
  187. return iter->second;
  188. return {};
  189. }
  190. auto begin() const { return stateForSubscription.begin(); }
  191. auto end() const { return stateForSubscription.end(); }
  192. private:
  193. RequestRetryQueue queue;
  194. std::map<Token64, SubscriptionState> stateForSubscription;
  195. };
  196. class SubscriptionManager::Impl : public std::enable_shared_from_this<Impl>,
  197. private DeviceListener
  198. {
  199. public:
  200. explicit Impl (SubscriptionManagerDelegate& d)
  201. : delegate (d) {}
  202. SubscriptionKey beginSubscription (MUID m, const PropertySubscriptionHeader& header)
  203. {
  204. const auto key = infoForMuid[m].beginSubscription (header);
  205. sendPendingMessages();
  206. return SubscriptionKey { m, key };
  207. }
  208. void endSubscription (SubscriptionKey key)
  209. {
  210. const auto iter = infoForMuid.find (key.getMuid());
  211. if (iter == infoForMuid.end())
  212. return;
  213. const auto ended = iter->second.endSubscription (key.getKey());
  214. if (! ended.has_value())
  215. return;
  216. if (auto* request = std::get_if<Token64> (&ended->state))
  217. {
  218. delegate.abortPropertyRequest ({ key.getMuid(), *request });
  219. }
  220. else if (auto* subscribeId = std::get_if<String> (&ended->state))
  221. {
  222. PropertySubscriptionHeader header;
  223. header.command = PropertySubscriptionCommand::end;
  224. header.subscribeId = *subscribeId;
  225. iter->second.postToQueue (header);
  226. sendPendingMessages();
  227. }
  228. }
  229. void endSubscriptionFromResponder (MUID m, String sub)
  230. {
  231. const auto iter = infoForMuid.find (m);
  232. if (iter != infoForMuid.end())
  233. for (const auto& ended : iter->second.endSubscription (sub))
  234. delegate.propertySubscriptionChanged ({ m, ended }, std::nullopt);
  235. }
  236. void endSubscriptionsFromResponder (MUID m)
  237. {
  238. const auto iter = infoForMuid.find (m);
  239. if (iter == infoForMuid.end())
  240. return;
  241. std::vector<Token64> tokens;
  242. std::transform (iter->second.begin(),
  243. iter->second.end(),
  244. std::back_inserter (tokens),
  245. [] (const auto& p) { return p.first; });
  246. iter->second.endAll();
  247. for (const auto& ended : tokens)
  248. delegate.propertySubscriptionChanged ({ m, ended }, std::nullopt);
  249. }
  250. std::vector<SubscriptionKey> getOngoingSubscriptions() const
  251. {
  252. std::vector<SubscriptionKey> result;
  253. for (const auto& pair : infoForMuid)
  254. for (const auto& info : pair.second)
  255. result.emplace_back (pair.first, info.first);
  256. return result;
  257. }
  258. std::optional<SubscriptionState> getInfoForSubscriptionKey (SubscriptionKey key) const
  259. {
  260. const auto iter = infoForMuid.find (key.getMuid());
  261. if (iter != infoForMuid.end())
  262. return iter->second.getInfoForSubscriptionKey (key.getKey());
  263. return {};
  264. }
  265. bool sendPendingMessages()
  266. {
  267. // Note: not using any_of here because we don't want the early-exit behaviour
  268. bool result = true;
  269. for (auto& pair : infoForMuid)
  270. result &= sendPendingMessages (pair.first, pair.second);
  271. return result;
  272. }
  273. private:
  274. void handleReply (SubscriptionKey subscriptionKey, PropertySubscriptionCommand command, const PropertyExchangeResult& r)
  275. {
  276. const auto iter = infoForMuid.find (subscriptionKey.getMuid());
  277. if (iter == infoForMuid.end())
  278. return;
  279. auto& second = iter->second;
  280. if (const auto error = r.getError())
  281. {
  282. // If the responder requested a retry, keep the message in the queue so that
  283. // it can be re-sent
  284. if (*error == PropertyExchangeResult::Error::tooManyTransactions)
  285. {
  286. second.resetKey (subscriptionKey.getKey());
  287. return;
  288. }
  289. // We tried to begin or end a subscription, but the responder said no!
  290. // If the responder declined to start a subscription, we can just
  291. // mark the subscription as ended.
  292. // If the responder declined to end a subscription, that's a bit trickier.
  293. // Hopefully this won't happen in practice, because all the options to resolve are pretty bad:
  294. // - One option is to ignore the failure. The remote device can carry on sending us updates.
  295. // This might be a bit dangerous if we repeatedly subscribe and then fail to unsubscribe, as this
  296. // would result in lots of redundant subscription messages that could clog the connection.
  297. // - Another option is to store the subscription-end request and to attempt to send it again later.
  298. // This also has the potential to clog up the connection, depending on how frequently we attempt
  299. // to re-send failed messages. Given that unsubscribing has already failed once, there's no
  300. // guarantee that any future attempts will succeed, so we might end up in a loop, sending the
  301. // same message over and over.
  302. // On balance, I think the former option is best for now. If this ends up being an issue in
  303. // practice, perhaps we could add a mechanism to do exponential back-off, but that would
  304. // add complexity that isn't necessarily required.
  305. jassert (*error != PropertyExchangeResult::Error::notify);
  306. // If we failed to begin a subscription, then the subscription never started,
  307. // and we should remove it from the set of ongoing subscriptions.
  308. second.endSubscription (subscriptionKey.getKey());
  309. // We only need to alert the delegate if the subscription failed to start.
  310. // If the subscription fails to end, we'll treat the subscription as ended anyway.
  311. if (command == PropertySubscriptionCommand::start)
  312. delegate.propertySubscriptionChanged (subscriptionKey, std::nullopt);
  313. return;
  314. }
  315. if (command == PropertySubscriptionCommand::start)
  316. {
  317. second.setSubscribeIdForKey (subscriptionKey.getKey(), r.getHeaderAsSubscriptionHeader().subscribeId);
  318. delegate.propertySubscriptionChanged (subscriptionKey, r.getHeaderAsSubscriptionHeader().subscribeId);
  319. }
  320. }
  321. bool sendPendingMessages (MUID m, DeviceSubscriptionStates& info)
  322. {
  323. while (auto* entry = info.markNextInFlight())
  324. {
  325. const auto requestKind = entry->msg.command;
  326. const SubscriptionKey subscriptionKey { m, entry->key };
  327. auto cb = [weak = weak_from_this(), requestKind, subscriptionKey] (const PropertyExchangeResult& r)
  328. {
  329. if (const auto locked = weak.lock())
  330. locked->handleReply (subscriptionKey, requestKind, r);
  331. };
  332. if (const auto request = delegate.sendPropertySubscribe (m, entry->msg, std::move (cb)))
  333. {
  334. if (entry->msg.command == PropertySubscriptionCommand::start)
  335. info.setRequestIdForKey (entry->key, request->getKey());
  336. }
  337. else
  338. {
  339. // Couldn't find a valid ID to use, so we must have exhausted all message slots.
  340. // There's no point trying to send the rest of the messages that are queued for this
  341. // MUID, so give up. It's probably a good idea to try again in a bit.
  342. info.resetKey (entry->key);
  343. return false;
  344. }
  345. }
  346. return true;
  347. }
  348. SubscriptionManagerDelegate& delegate;
  349. std::map<MUID, DeviceSubscriptionStates> infoForMuid;
  350. };
  351. //==============================================================================
  352. SubscriptionManager::SubscriptionManager (SubscriptionManagerDelegate& delegate)
  353. : pimpl (std::make_shared<Impl> (delegate)) {}
  354. SubscriptionKey SubscriptionManager::beginSubscription (MUID m, const PropertySubscriptionHeader& header)
  355. {
  356. return pimpl->beginSubscription (m, header);
  357. }
  358. void SubscriptionManager::endSubscription (SubscriptionKey key)
  359. {
  360. pimpl->endSubscription (key);
  361. }
  362. void SubscriptionManager::endSubscriptionFromResponder (MUID m, String sub)
  363. {
  364. pimpl->endSubscriptionFromResponder (m, sub);
  365. }
  366. void SubscriptionManager::endSubscriptionsFromResponder (MUID m)
  367. {
  368. pimpl->endSubscriptionsFromResponder (m);
  369. }
  370. std::vector<SubscriptionKey> SubscriptionManager::getOngoingSubscriptions() const
  371. {
  372. return pimpl->getOngoingSubscriptions();
  373. }
  374. std::optional<String> SubscriptionManager::getSubscribeIdForKey (SubscriptionKey key) const
  375. {
  376. if (const auto info = pimpl->getInfoForSubscriptionKey (key))
  377. if (const auto* subscribeId = std::get_if<String> (&info->state))
  378. return *subscribeId;
  379. return {};
  380. }
  381. std::optional<String> SubscriptionManager::getResourceForKey (SubscriptionKey key) const
  382. {
  383. if (const auto info = pimpl->getInfoForSubscriptionKey (key))
  384. return info->resource;
  385. return {};
  386. }
  387. bool SubscriptionManager::sendPendingMessages()
  388. {
  389. return pimpl->sendPendingMessages();
  390. }
  391. //==============================================================================
  392. //==============================================================================
  393. #if JUCE_UNIT_TESTS
  394. class SubscriptionTests : public UnitTest
  395. {
  396. public:
  397. SubscriptionTests() : UnitTest ("Subscription", UnitTestCategories::midi) {}
  398. void runTest() override
  399. {
  400. auto random = getRandom();
  401. class Delegate : public SubscriptionManagerDelegate
  402. {
  403. public:
  404. std::optional<RequestKey> sendPropertySubscribe (MUID m,
  405. const PropertySubscriptionHeader&,
  406. std::function<void (const PropertyExchangeResult&)> cb) override
  407. {
  408. ++sendCount;
  409. if (! sendShouldSucceed)
  410. return {};
  411. const RequestKey key { m, Token64 { ++lastKey } };
  412. callbacks[key] = std::move (cb);
  413. return key;
  414. }
  415. void abortPropertyRequest (RequestKey k) override
  416. {
  417. ++abortCount;
  418. callbacks.erase (k);
  419. }
  420. void propertySubscriptionChanged (SubscriptionKey, const std::optional<String>&) override
  421. {
  422. subChanged = true;
  423. }
  424. void setSendShouldSucceed (bool b) { sendShouldSucceed = b; }
  425. void sendResult (RequestKey key, const PropertyExchangeResult& r)
  426. {
  427. const auto iter = callbacks.find (key);
  428. if (iter != callbacks.end())
  429. NullCheckedInvocation::invoke (iter->second, r);
  430. callbacks.erase (key);
  431. }
  432. std::vector<RequestKey> getOngoingRequests() const
  433. {
  434. std::vector<RequestKey> result;
  435. result.reserve (callbacks.size());
  436. std::transform (callbacks.begin(), callbacks.end(), std::back_inserter (result), [] (const auto& p) { return p.first; });
  437. return result;
  438. }
  439. uint64_t getAndClearSendCount() { return std::exchange (sendCount, 0); }
  440. uint64_t getAndClearAbortCount() { return std::exchange (abortCount, 0); }
  441. bool getAndClearSubChanged() { return std::exchange (subChanged, false); }
  442. private:
  443. std::map<RequestKey, std::function<void (const PropertyExchangeResult&)>> callbacks;
  444. uint64_t sendCount = 0, abortCount = 0;
  445. uint64_t lastKey = 0;
  446. bool sendShouldSucceed = true, subChanged = false;
  447. };
  448. Delegate delegate;
  449. SubscriptionManager manager { delegate };
  450. const auto inquiryMUID = MUID::makeRandom (random);
  451. beginTest ("Beginning a subscription and ending it before the remote device replies aborts the request");
  452. {
  453. PropertySubscriptionHeader header;
  454. header.command = PropertySubscriptionCommand::start;
  455. header.resource = "X-CustomProp";
  456. const auto a = manager.beginSubscription (inquiryMUID, header);
  457. expect (manager.getOngoingSubscriptions() == std::vector { a });
  458. expect (delegate.getAndClearSendCount() == 1);
  459. // Sending a subscription request uses a request slot
  460. expect (delegate.getOngoingRequests().size() == 1);
  461. const auto request = delegate.getOngoingRequests().back();
  462. // subscription id is empty until the responder confirms the subscription
  463. expect (manager.getResourceForKey (a) == header.resource);
  464. manager.endSubscription (a);
  465. expect (delegate.getOngoingRequests().empty());
  466. expect (delegate.getAndClearAbortCount() == 1);
  467. expect (manager.getOngoingSubscriptions().empty());
  468. const auto successHeader = []
  469. {
  470. auto ptr = std::make_unique<DynamicObject>();
  471. ptr->setProperty ("status", 200);
  472. ptr->setProperty ("subscribeId", "anId");
  473. return var { ptr.release() };
  474. }();
  475. delegate.sendResult (request, PropertyExchangeResult { successHeader, {} });
  476. // Already ended, the confirmation shouldn't do anything
  477. expect (! delegate.getAndClearSubChanged());
  478. expect (delegate.getOngoingRequests().empty());
  479. expect (delegate.getAndClearSendCount() == 0);
  480. expect (delegate.getAndClearAbortCount() == 0);
  481. // There shouldn't be any queued messages.
  482. expect (manager.sendPendingMessages());
  483. expect (! delegate.getAndClearSubChanged());
  484. expect (delegate.getOngoingRequests().empty());
  485. expect (delegate.getAndClearSendCount() == 0);
  486. expect (delegate.getAndClearAbortCount() == 0);
  487. }
  488. beginTest ("Starting a new subscription while the device is waiting for a previous subscription to be confirmed queues further requests");
  489. {
  490. PropertySubscriptionHeader header;
  491. header.command = PropertySubscriptionCommand::start;
  492. header.resource = "X-CustomProp";
  493. delegate.setSendShouldSucceed (false);
  494. const auto a = manager.beginSubscription (inquiryMUID, header);
  495. expect (delegate.getAndClearSendCount() == 1);
  496. expect (! manager.sendPendingMessages());
  497. expect (delegate.getAndClearSendCount() == 1);
  498. delegate.setSendShouldSucceed (true);
  499. expect (manager.sendPendingMessages());
  500. expect (delegate.getAndClearSendCount() == 1);
  501. delegate.setSendShouldSucceed (false);
  502. const auto b = manager.beginSubscription (inquiryMUID, header);
  503. const auto c = manager.beginSubscription (inquiryMUID, header);
  504. expect (manager.getOngoingSubscriptions() == std::vector { a, b, c });
  505. expect (delegate.getOngoingRequests().size() == 1);
  506. // subscription id is empty until the responder confirms the subscription
  507. expect (manager.getResourceForKey (a) == header.resource);
  508. expect (manager.getResourceForKey (b) == header.resource);
  509. expect (manager.getResourceForKey (c) == header.resource);
  510. expect (delegate.getAndClearSendCount() == 2);
  511. expect (delegate.getAndClearAbortCount() == 0);
  512. delegate.setSendShouldSucceed (true);
  513. // The device has sent a subscription start for a, but not for c,
  514. // so it should send a notify to end subscription a, but shouldn't emit any
  515. // messages related to subscription c.
  516. manager.endSubscription (a);
  517. manager.endSubscription (c);
  518. expect (manager.getOngoingSubscriptions() == std::vector { b });
  519. expect (delegate.getOngoingRequests().empty());
  520. expect (delegate.getAndClearSendCount() == 0);
  521. expect (delegate.getAndClearAbortCount() == 1);
  522. // There should still be requests related to subscription b pending
  523. expect (manager.sendPendingMessages());
  524. expect (delegate.getOngoingRequests().size() == 1);
  525. expect (delegate.getAndClearSendCount() == 1);
  526. expect (delegate.getAndClearAbortCount() == 0);
  527. // Now, we should send a terminate request for subscription b
  528. manager.endSubscription (b);
  529. expect (delegate.getOngoingRequests().empty());
  530. expect (delegate.getAndClearSendCount() == 0);
  531. expect (delegate.getAndClearAbortCount() == 1);
  532. // The manager never received any replies, so it shouldn't have notified listeners about
  533. // changed subscriptions
  534. expect (! delegate.getAndClearSubChanged());
  535. }
  536. beginTest ("If the device receives a retry or notify in response to a subscription start request, the subscription is retried or terminated as necessary");
  537. {
  538. PropertySubscriptionHeader header;
  539. header.command = PropertySubscriptionCommand::start;
  540. header.resource = "X-CustomProp";
  541. const auto a = manager.beginSubscription (inquiryMUID, header);
  542. expect (manager.getOngoingSubscriptions() == std::vector { a });
  543. expect (delegate.getOngoingRequests().size() == 1);
  544. expect (delegate.getAndClearSendCount() == 1);
  545. expect (delegate.getAndClearAbortCount() == 0);
  546. delegate.sendResult (delegate.getOngoingRequests().back(), PropertyExchangeResult { PropertyExchangeResult::Error::tooManyTransactions });
  547. // The subscription is still active from the perspective of the manager, but the
  548. // first request is over and should be retried
  549. expect (manager.getOngoingSubscriptions() == std::vector { a });
  550. expect (! delegate.getAndClearSubChanged());
  551. expect (delegate.getOngoingRequests().empty());
  552. expect (delegate.getAndClearSendCount() == 0);
  553. expect (delegate.getAndClearAbortCount() == 0);
  554. expect (manager.sendPendingMessages());
  555. expect (manager.getOngoingSubscriptions() == std::vector { a });
  556. expect (delegate.getOngoingRequests().size() == 1);
  557. expect (delegate.getAndClearSendCount() == 1);
  558. expect (delegate.getAndClearAbortCount() == 0);
  559. delegate.sendResult (delegate.getOngoingRequests().back(), PropertyExchangeResult { PropertyExchangeResult::Error::notify });
  560. // The request was terminated by the responder, so the delegate should get a sub-changed message
  561. expect (delegate.getAndClearSubChanged());
  562. expect (manager.getOngoingSubscriptions().empty());
  563. expect (delegate.getOngoingRequests().empty());
  564. expect (delegate.getAndClearSendCount() == 0);
  565. expect (delegate.getAndClearAbortCount() == 0);
  566. expect (manager.sendPendingMessages());
  567. expect (! delegate.getAndClearSubChanged());
  568. expect (manager.getOngoingSubscriptions().empty());
  569. expect (delegate.getOngoingRequests().empty());
  570. expect (delegate.getAndClearSendCount() == 0);
  571. expect (delegate.getAndClearAbortCount() == 0);
  572. }
  573. beginTest ("If the device receives a retry or notify in response to a subscription end request, the subscription is retried as necessary");
  574. {
  575. PropertySubscriptionHeader header;
  576. header.command = PropertySubscriptionCommand::start;
  577. header.resource = "X-CustomProp";
  578. const auto a = manager.beginSubscription (inquiryMUID, header);
  579. expect (manager.getOngoingSubscriptions() == std::vector { a });
  580. expect (manager.getResourceForKey (a) == header.resource);
  581. expect (! manager.getSubscribeIdForKey (a).has_value());
  582. const auto subscriptionResponseHeader = []
  583. {
  584. auto ptr = std::make_unique<DynamicObject>();
  585. ptr->setProperty ("status", 200);
  586. ptr->setProperty ("subscribeId", "newId");
  587. return ptr.release();
  588. }();
  589. // Accept the subscription
  590. delegate.sendResult (delegate.getOngoingRequests().back(), PropertyExchangeResult { subscriptionResponseHeader, {} });
  591. // The subscription is still active from the perspective of the device, but the
  592. // request is over and should be retried
  593. expect (manager.getOngoingSubscriptions() == std::vector { a });
  594. expect (delegate.getAndClearSubChanged());
  595. // Now that the subscription was accepted, the subscription id should be non-empty
  596. expect (manager.getResourceForKey (a) == header.resource);
  597. expect (manager.getSubscribeIdForKey (a) == "newId");
  598. expect (delegate.getOngoingRequests().empty());
  599. expect (delegate.getAndClearSendCount() == 1);
  600. expect (delegate.getAndClearAbortCount() == 0);
  601. manager.endSubscription (a);
  602. expect (manager.getOngoingSubscriptions().empty());
  603. expect (! delegate.getAndClearSubChanged());
  604. expect (delegate.getOngoingRequests().size() == 1);
  605. expect (delegate.getAndClearSendCount() == 1);
  606. expect (delegate.getAndClearAbortCount() == 0);
  607. // The responder is busy, can't process the subscription end
  608. delegate.sendResult (delegate.getOngoingRequests().back(), PropertyExchangeResult { PropertyExchangeResult::Error::tooManyTransactions });
  609. expect (manager.getOngoingSubscriptions().empty());
  610. expect (delegate.getOngoingRequests().empty());
  611. expect (delegate.getAndClearSendCount() == 0);
  612. expect (delegate.getAndClearAbortCount() == 0);
  613. expect (manager.sendPendingMessages());
  614. expect (manager.getOngoingSubscriptions().empty());
  615. expect (delegate.getOngoingRequests().size() == 1);
  616. expect (delegate.getAndClearSendCount() == 1);
  617. expect (delegate.getAndClearAbortCount() == 0);
  618. // The responder told us to immediately terminate our request to end the subscription!
  619. // It's unclear how this should behave, so we'll just ignore the failure and assume
  620. // the subscription is really over.
  621. delegate.sendResult (delegate.getOngoingRequests().back(), PropertyExchangeResult { PropertyExchangeResult::Error::notify });
  622. expect (manager.getOngoingSubscriptions().empty());
  623. expect (delegate.getOngoingRequests().empty());
  624. expect (delegate.getAndClearSendCount() == 0);
  625. expect (delegate.getAndClearAbortCount() == 0);
  626. expect (manager.sendPendingMessages());
  627. expect (delegate.getAndClearSendCount() == 0);
  628. expect (delegate.getAndClearAbortCount() == 0);
  629. expect (! delegate.getAndClearSubChanged());
  630. }
  631. }
  632. };
  633. static SubscriptionTests subscriptionTests;
  634. #endif
  635. } // namespace juce::midi_ci