Source: client/firebase-subscription-service.js

  1. /*
  2. * Copyright 2023, TeamDev. All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Redistribution and use in source and/or binary forms, with or without
  11. * modification, must retain the above copyright notice and the following
  12. * disclaimer.
  13. *
  14. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  15. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  16. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  17. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  18. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  19. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  20. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  21. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  22. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  23. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  24. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  25. */
  26. "use strict";
  27. import {Duration} from './time-utils';
  28. import ObjectToProto from './object-to-proto';
  29. import {Status} from '../proto/spine/core/response_pb';
  30. /**
  31. * The default interval for sending subscription keep up requests.
  32. *
  33. * @type {Duration}
  34. */
  35. const DEFAULT_KEEP_UP_INTERVAL = new Duration({minutes: 2});
  36. /**
  37. * A service that manages the active subscriptions periodically sending requests
  38. * to keep them running.
  39. */
  40. export class FirebaseSubscriptionService {
  41. /**
  42. * @param {Endpoint} endpoint an endpoint to communicate with
  43. * @param {?Duration} keepUpInterval a custom interval for sending subscription keep up requests
  44. */
  45. constructor(endpoint, keepUpInterval) {
  46. /**
  47. * @type {SpineSubscription[]}
  48. * @private
  49. */
  50. this._subscriptions = [];
  51. /**
  52. * @type {Endpoint}
  53. * @private
  54. */
  55. this._endpoint = endpoint;
  56. /**
  57. * @type {Duration}
  58. * @private
  59. */
  60. this._keepUpInterval = keepUpInterval
  61. ? keepUpInterval
  62. : DEFAULT_KEEP_UP_INTERVAL;
  63. }
  64. /**
  65. * Add a subscription to the service to handle the keep-up requests and cancel in
  66. * case of unsubscribe.
  67. *
  68. * @param {SpineSubscription} subscription an active subscription to keep running
  69. */
  70. add(subscription) {
  71. if (this._isRegistered(subscription)) {
  72. throw new Error('This subscription is already registered in subscription service');
  73. }
  74. this._subscriptions.push(subscription);
  75. if (!this._isRunning()) {
  76. this._run();
  77. }
  78. }
  79. /**
  80. * Immediately cancels all active subscriptions previously created through this service.
  81. */
  82. cancelAllSubscriptions() {
  83. const activeSubscriptions = this._subscriptions.filter(s => !s.closed);
  84. if (activeSubscriptions.length > 0) {
  85. const subscriptionMessages = activeSubscriptions.map(s => s.internal())
  86. this._endpoint.cancelAll(subscriptionMessages);
  87. activeSubscriptions.forEach(s => {
  88. s.unsubscribe() /* Calling RxJS's `unsubscribe` to stop propagating the updates. */
  89. this._removeSubscription(s)
  90. })
  91. }
  92. }
  93. /**
  94. * Immediately cancels the given subscription, including cancelling it on the server-side.
  95. *
  96. * @param {SubscriptionObject} subscription the subscription to cancel
  97. */
  98. cancelSubscription(subscription) {
  99. this._endpoint.cancelSubscription(subscription)
  100. .then(() => {
  101. this._removeSubscription(subscription)
  102. });
  103. }
  104. /**
  105. * Indicates whether this service is running keeping up subscriptions.
  106. *
  107. * @returns {boolean}
  108. * @private
  109. */
  110. _isRunning() {
  111. return !!this._interval;
  112. }
  113. /**
  114. * Starts the subscription service, keeping up the added subscriptions.
  115. *
  116. * @private
  117. */
  118. _run() {
  119. this._interval = setInterval(() => {
  120. this._keepUpSubscriptions();
  121. }, this._keepUpInterval.inMs());
  122. }
  123. /**
  124. * Stops the subscription service.
  125. *
  126. * @private
  127. */
  128. _stop() {
  129. clearInterval(this._interval);
  130. this._interval = null;
  131. }
  132. /**
  133. * Sends the "keep-up" request for all active subscriptions.
  134. *
  135. * The non-`OK` response status means the subscription has already been canceled on the server,
  136. * most likely due to a timeout. So, in such case, the subscription is removed from the list of
  137. * active ones.
  138. *
  139. * @private
  140. */
  141. _keepUpSubscriptions() {
  142. const cancelledSubscriptions = this._subscriptions.filter(s => s.closed);
  143. if (cancelledSubscriptions.length > 0) {
  144. const subscriptionMessages = cancelledSubscriptions.map(s => s.internal())
  145. this._endpoint.cancelAll(subscriptionMessages);
  146. cancelledSubscriptions.forEach(s => this._removeSubscription(s))
  147. }
  148. const subscriptions = this._subscriptions.map(value => value.internal());
  149. if (subscriptions.length === 0) {
  150. return;
  151. }
  152. this._endpoint.keepUpSubscriptions(subscriptions).then(response => {
  153. for (let i = 0; i < response.response.length; i++) {
  154. const r = response.response[i];
  155. const status = ObjectToProto.convert(r.status, Status.typeUrl());
  156. if (status.getStatusCase() !== Status.StatusCase.OK) {
  157. this._removeSubscription(subscriptions[i])
  158. }
  159. }
  160. });
  161. }
  162. /**
  163. * Removes the provided subscription from subscriptions list, which stops any attempts
  164. * to update it. In case no more subscriptions are left, stops this service.
  165. *
  166. * In case the passed subscription is not known to this service, does nothing.
  167. *
  168. * @param subscription a subscription to cancel;
  169. * this method accepts values of both `SpineSubscription`
  170. * and Proto `Subscription` types,
  171. * and operates based on the subscription ID
  172. * @private
  173. */
  174. _removeSubscription(subscription) {
  175. let id;
  176. if (typeof subscription.id === 'function') {
  177. id = subscription.id();
  178. } else {
  179. id = subscription.getId().getValue();
  180. }
  181. const index = this._subscriptions.findIndex(item => item.id() === id);
  182. this._subscriptions.splice(index, 1);
  183. if (this._subscriptions.length === 0) {
  184. this._stop();
  185. }
  186. }
  187. /**
  188. * @private
  189. */
  190. _isRegistered(subscription) {
  191. const id = subscription.id();
  192. const exists = this._subscriptions.find(registered => registered.id() === id);
  193. return !!exists;
  194. }
  195. }