Source: client/firebase-database-client.js

/*
 * Copyright 2023, TeamDev. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Redistribution and use in source and/or binary forms, with or without
 * modification, must retain the above copyright notice and the following
 * disclaimer.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

"use strict";

import {Subscription, Subject} from 'rxjs';
import { ref, onValue,
  onChildAdded, onChildRemoved, onChildChanged, onChildMoved } from "firebase/database";

/**
 * The client of a Firebase Realtime database.
 */
export class FirebaseDatabaseClient {

  /**
   * Creates a new FirebaseDatabaseClient.
   *
   * @param {!firebase.database.Database} database a database of the initialized Firebase application
   */
  constructor(database) {
    this._database = database;
  }

  /**
   * Subscribes to the `child_added` events of the node under the given path.
   *
   * Each child's value is parsed as a JSON and dispatched to the given callback
   *
   * @param {!string} path the path to the watched node
   * @param {!Subject<Object>} dataSubject the subject receiving child values
   *
   * @return {Subscription} a Subscription that can be unsubscribed
   */
  onChildAdded(path, dataSubject) {
    return this._subscribeToChildEvent('child_added', path, dataSubject);
  }

  /**
   * Subscribes to the `child_changed` events of the node under the given path.
   *
   * Each child's value is parsed as a JSON and dispatched to the given callback
   *
   * @param {!string} path the path to the watched node
   * @param {!Subject<Object>} dataSubject the subject receiving child values
   *
   * @return {Subscription} a Subscription that can be unsubscribed
   */
  onChildChanged(path, dataSubject) {
    return this._subscribeToChildEvent('child_changed', path, dataSubject);
  }

  /**
   * Subscribes to the `child_removed` events of the node under the given path.
   *
   * Each child's value is parsed as a JSON and dispatched to the given callback
   *
   * @param {!string} path the path to the watched node
   * @param {!Subject<Object>} dataSubject the subject receiving child values
   *
   * @return {Subscription} a Subscription that can be unsubscribed
   */
  onChildRemoved(path, dataSubject) {
    return this._subscribeToChildEvent('child_removed', path, dataSubject);
  }

  _subscribeToChildEvent(childEvent, path, dataSubject) {
    const dbRef = ref(this._database, path);
    let valueCallback = response => {
      const msgJson = response.val();
      const message = JSON.parse(msgJson);
      dataSubject.next(message);
    };
    let offCallback = null;
    switch (childEvent) {
      case 'child_added':
        offCallback = onChildAdded(dbRef, valueCallback);
        break;
      case 'child_removed':
        offCallback = onChildRemoved(dbRef, valueCallback);
        break;
      case 'child_changed':
        offCallback = onChildChanged(dbRef, valueCallback);
        break;
      case 'child_moved':
        offCallback = onChildMoved(dbRef, valueCallback);
        break;
      default:
        throw new Error('Invalid child event: ' + childEvent);
    }
    return new Subscription(() => {
      offCallback?.();
      dataSubject.complete();
    });
  }

  /**
   * Gets an array of values from Firebase at the provided path.
   *
   * @param {!string} path the path to the node to get value from
   * @param {!consumerCallback<Object[]>} dataCallback a callback which is invoked with an array of
   *                                                   entities at path
   */
  getValues(path, dataCallback) {
    const dbRef = ref(this._database, path);
    onValue(dbRef, response => {
      const data = response.val(); // an Object mapping Firebase ids to objects is returned
      if (data == null) {
        return dataCallback([]);
      }
      const objectStrings = Object.values(data);
      const items = objectStrings.map(item => JSON.parse(item));
      dataCallback(items);
    }, {
      onlyOnce: true
    })
  }
}