Source: client/firebase-client.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 {Observable, Subject, Subscription} from 'rxjs';
  28. import {
  29. Subscription as SubscriptionObject,
  30. SubscriptionId
  31. } from '../proto/spine/client/subscription_pb';
  32. import {ActorRequestFactory} from './actor-request-factory';
  33. import {AbstractClientFactory} from './client-factory';
  34. import {CommandingClient} from "./commanding-client";
  35. import {CompositeClient} from "./composite-client";
  36. import {HttpEndpoint} from './http-endpoint';
  37. import {FirebaseDatabaseClient} from './firebase-database-client';
  38. import {FirebaseSubscriptionService} from './firebase-subscription-service';
  39. import ObjectToProto from './object-to-proto';
  40. import {QueryingClient} from "./querying-client";
  41. import {SubscribingClient} from "./subscribing-client";
  42. /**
  43. * An abstract base for subscription objects.
  44. *
  45. * @abstract
  46. */
  47. class SpineSubscription extends Subscription {
  48. /**
  49. * @param {Function} unsubscribe the callbacks that allows to cancel the subscription
  50. * @param {SubscriptionObject} subscription the wrapped subscription object
  51. *
  52. * @protected
  53. */
  54. constructor(unsubscribe, subscription) {
  55. super(unsubscribe);
  56. this._subscription = subscription;
  57. }
  58. /**
  59. * An internal Spine subscription which includes the topic the updates are received for.
  60. *
  61. * @return {SubscriptionObject} a `spine.client.Subscription` instance
  62. */
  63. internal() {
  64. return this._subscription;
  65. }
  66. /**
  67. * @return {String} a string value of the `internal` subscription ID.
  68. */
  69. id() {
  70. return this.internal().getId().getValue();
  71. }
  72. }
  73. /**
  74. * A subscription to entity changes on application backend.
  75. */
  76. class EntitySubscription extends SpineSubscription {
  77. /**
  78. * @param {Function} unsubscribe the callback that allows to cancel the subscription
  79. * @param {{itemAdded: Observable, itemChanged: Observable, itemRemoved: Observable}} observables
  80. * the observables for entity change
  81. * @param {SubscriptionObject} subscription the wrapped subscription object
  82. */
  83. constructor({
  84. unsubscribedBy: unsubscribe,
  85. withObservables: observables,
  86. forInternal: subscription
  87. }) {
  88. super(unsubscribe, subscription);
  89. this._observables = observables;
  90. }
  91. /**
  92. * @return {EntitySubscriptionObject} a plain object with observables and unsubscribe method
  93. */
  94. toObject() {
  95. return Object.assign({},
  96. this._observables,
  97. {
  98. unsubscribe: () => {
  99. return this.unsubscribe();
  100. }
  101. });
  102. }
  103. }
  104. /**
  105. * A subscription to events that occur in the system.
  106. */
  107. class EventSubscription extends SpineSubscription {
  108. /**
  109. * @param {Function} unsubscribe the callbacks that allows to cancel the subscription
  110. * @param {Observable} eventEmitted the observable for the emitted events
  111. * @param {SubscriptionObject} subscription the wrapped subscription object
  112. */
  113. constructor({
  114. unsubscribedBy: unsubscribe,
  115. withObservable: observable,
  116. forInternal: subscription
  117. }) {
  118. super(unsubscribe, subscription);
  119. this._observable = observable;
  120. }
  121. /**
  122. * @return {EventSubscriptionObject} a plain object with observables and unsubscribe method
  123. */
  124. toObject() {
  125. return Object.assign(
  126. {},
  127. {eventEmitted: this._observable},
  128. {
  129. unsubscribe: () => {
  130. return this.unsubscribe();
  131. }
  132. }
  133. );
  134. }
  135. }
  136. class FirebaseQueryingClient extends QueryingClient {
  137. /**
  138. * Creates an instance of the client.
  139. *
  140. * @param {!HttpEndpoint} endpoint the server endpoint to execute queries and commands
  141. * @param {!FirebaseDatabaseClient} firebaseDatabase the client to read the query results from
  142. * @param {!ActorRequestFactory} actorRequestFactory a factory to instantiate the actor requests with
  143. */
  144. constructor(endpoint, firebaseDatabase, actorRequestFactory) {
  145. super(actorRequestFactory);
  146. this._endpoint = endpoint;
  147. this._firebase = firebaseDatabase;
  148. }
  149. read(query) {
  150. return new Promise((resolve, reject) => {
  151. this._endpoint.query(query)
  152. .then(({path}) => this._firebase.getValues(path, values => {
  153. const typeUrl = query.getTarget().getType();
  154. const messages = values.map(value => ObjectToProto.convert(value, typeUrl));
  155. resolve(messages);
  156. }))
  157. .catch(error => reject(error));
  158. });
  159. }
  160. }
  161. const EVENT_TYPE_URL = 'type.spine.io/spine.core.Event';
  162. /**
  163. * A {@link SubscribingClient} which receives entity state updates and events from
  164. * a Firebase Realtime Database.
  165. */
  166. class FirebaseSubscribingClient extends SubscribingClient {
  167. /**
  168. * Creates an instance of the client.
  169. *
  170. * @param {!HttpEndpoint} endpoint
  171. * the server endpoint to execute queries and commands
  172. * @param {!FirebaseDatabaseClient} firebaseDatabase
  173. * the client to read the query results from
  174. * @param {!ActorRequestFactory} actorRequestFactory
  175. * a factory to instantiate the actor requests with
  176. * @param {!FirebaseSubscriptionService} subscriptionService
  177. * a service handling the subscriptions
  178. */
  179. constructor(endpoint, firebaseDatabase, actorRequestFactory, subscriptionService) {
  180. super(actorRequestFactory);
  181. this._endpoint = endpoint;
  182. this._firebase = firebaseDatabase;
  183. this._subscriptionService = subscriptionService;
  184. }
  185. /**
  186. * @inheritDoc
  187. */
  188. subscribe(topic) {
  189. return this._doSubscribe(topic, this._entitySubscription);
  190. }
  191. /**
  192. * @inheritDoc
  193. */
  194. subscribeToEvents(topic) {
  195. return this._doSubscribe(topic, this._eventSubscription);
  196. }
  197. /**
  198. * @private
  199. */
  200. _doSubscribe(topic, createSubscriptionFn) {
  201. return new Promise((resolve, reject) => {
  202. this._endpoint.subscribeTo(topic)
  203. .then(response => {
  204. const path = response.nodePath.value;
  205. const internalSubscription =
  206. FirebaseSubscribingClient.internalSubscription(path, topic);
  207. const subscription = createSubscriptionFn.call(this, path, internalSubscription);
  208. resolve(subscription.toObject());
  209. this._subscriptionService.add(subscription);
  210. })
  211. .catch(reject);
  212. });
  213. }
  214. /**
  215. * @private
  216. */
  217. _entitySubscription(path, subscription) {
  218. const itemAdded = new Subject();
  219. const itemChanged = new Subject();
  220. const itemRemoved = new Subject();
  221. const pathSubscriptions = [
  222. this._firebase.onChildAdded(path, itemAdded),
  223. this._firebase.onChildChanged(path, itemChanged),
  224. this._firebase.onChildRemoved(path, itemRemoved)
  225. ];
  226. const typeUrl = subscription.getTopic().getTarget().getType();
  227. return new EntitySubscription({
  228. unsubscribedBy: () => {
  229. FirebaseSubscribingClient._unsubscribe(pathSubscriptions);
  230. this._subscriptionService.cancelSubscription(subscription);
  231. },
  232. withObservables: {
  233. itemAdded: ObjectToProto.map(itemAdded.asObservable(), typeUrl),
  234. itemChanged: ObjectToProto.map(itemChanged.asObservable(), typeUrl),
  235. itemRemoved: ObjectToProto.map(itemRemoved.asObservable(), typeUrl)
  236. },
  237. forInternal: subscription
  238. });
  239. }
  240. /**
  241. * @private
  242. */
  243. _eventSubscription(path, subscription) {
  244. const itemAdded = new Subject();
  245. const pathSubscription = this._firebase.onChildAdded(path, itemAdded);
  246. return new EventSubscription({
  247. unsubscribedBy: () => {
  248. FirebaseSubscribingClient._unsubscribe([pathSubscription]);
  249. this._subscriptionService.cancelSubscription(subscription);
  250. },
  251. withObservable: ObjectToProto.map(itemAdded.asObservable(), EVENT_TYPE_URL),
  252. forInternal: subscription
  253. });
  254. }
  255. /**
  256. * @override
  257. */
  258. cancelAllSubscriptions() {
  259. this._subscriptionService.cancelAllSubscriptions();
  260. }
  261. /**
  262. * Unsubscribes the provided Firebase subscriptions.
  263. *
  264. * @param {Array<Subscription>} subscriptions
  265. * @private
  266. */
  267. static _unsubscribe(subscriptions) {
  268. subscriptions.forEach(subscription => {
  269. if (!subscription.closed) {
  270. subscription.unsubscribe();
  271. }
  272. });
  273. }
  274. /**
  275. * Creates a `SubscriptionObject` instance to communicate with Spine server.
  276. *
  277. * @param {!String} path a path to object which gets updated in Firebase
  278. * @param {!spine.client.Topic} topic a topic for which the Subscription gets updates
  279. * @return {SubscriptionObject} a `SubscriptionObject` instance to communicate with Spine server
  280. */
  281. static internalSubscription(path, topic) {
  282. const subscription = new SubscriptionObject();
  283. const id = new SubscriptionId();
  284. id.setValue(path);
  285. subscription.setId(id);
  286. subscription.setTopic(topic);
  287. return subscription;
  288. }
  289. }
  290. /**
  291. * An implementation of the `AbstractClientFactory` that creates instances of client which exchanges
  292. * data with the server via Firebase Realtime Database.
  293. */
  294. export class FirebaseClientFactory extends AbstractClientFactory {
  295. /**
  296. * Creates a new `FirebaseClient` instance which will send the requests on behalf of the provided
  297. * actor to the provided endpoint, retrieving the data from the provided Firebase storage.
  298. *
  299. * Expects that given options contain backend endpoint URL, firebase Database instance and
  300. * the actor provider.
  301. *
  302. * @param {ClientOptions} options
  303. * @return {Client} a new backend client instance which will send the requests on behalf
  304. * of the provided actor to the provided endpoint, retrieving the data
  305. * from the provided Firebase storage
  306. * @override
  307. */
  308. static _clientFor(options) {
  309. const httpClient = this._createHttpClient(options);
  310. const httpResponseHandler = this._createHttpResponseHandler(options);
  311. const endpoint = new HttpEndpoint(httpClient, httpResponseHandler, options.routing);
  312. const firebaseDatabaseClient = new FirebaseDatabaseClient(options.firebaseDatabase);
  313. const requestFactory = ActorRequestFactory.create(options);
  314. const subscriptionService =
  315. new FirebaseSubscriptionService(endpoint, options.subscriptionKeepUpInterval);
  316. const querying = new FirebaseQueryingClient(endpoint, firebaseDatabaseClient, requestFactory);
  317. const subscribing = new FirebaseSubscribingClient(endpoint,
  318. firebaseDatabaseClient,
  319. requestFactory,
  320. subscriptionService);
  321. const commanding = new CommandingClient(endpoint, requestFactory);
  322. return new CompositeClient(querying, subscribing, commanding);
  323. }
  324. static createQuerying(options) {
  325. const httpClient = this._createHttpClient(options);
  326. const httpResponseHandler = this._createHttpResponseHandler(options);
  327. const endpoint = new HttpEndpoint(httpClient, httpResponseHandler, options.routing);
  328. const firebaseDatabaseClient = new FirebaseDatabaseClient(options.firebaseDatabase);
  329. const requestFactory = ActorRequestFactory.create(options);
  330. return new FirebaseQueryingClient(endpoint, firebaseDatabaseClient, requestFactory);
  331. }
  332. static createSubscribing(options) {
  333. const httpClient = this._createHttpClient(options);
  334. const httpResponseHandler = this._createHttpResponseHandler(options);
  335. const endpoint = new HttpEndpoint(httpClient, httpResponseHandler, options.routing);
  336. const firebaseDatabaseClient = new FirebaseDatabaseClient(options.firebaseDatabase);
  337. const requestFactory = ActorRequestFactory.create(options);
  338. const subscriptionService =
  339. new FirebaseSubscriptionService(endpoint, options.subscriptionKeepUpInterval);
  340. return new FirebaseSubscribingClient(endpoint,
  341. firebaseDatabaseClient,
  342. requestFactory,
  343. subscriptionService);
  344. }
  345. /**
  346. * @override
  347. */
  348. static _ensureOptionsSufficient(options) {
  349. super._ensureOptionsSufficient(options);
  350. const messageForMissing = (option) =>
  351. `Unable to initialize Client with Firebase storage. The ClientOptions.${option} not specified.`;
  352. if (!options.endpointUrl) {
  353. throw new Error(messageForMissing('endpointUrl'));
  354. }
  355. if (!options.firebaseDatabase) {
  356. throw new Error(messageForMissing('firebaseDatabase'));
  357. }
  358. if (!options.actorProvider) {
  359. throw new Error(messageForMissing('actorProvider'));
  360. }
  361. }
  362. }