listener.js

/**
 * A Fireproof database Listener allows you to react to events in the database.
 *
 * @class Listener
 * @classdesc An listener attaches to a Fireproof database and runs a routing function on each change, sending the results to subscribers.
 *
 * @param {Fireproof} database - The Fireproof database instance to index.
 * @param {Function} routingFn - The routing function to apply to each entry in the database.
 */
// import { ChangeEvent } from './db-index'

export default class Listener {
  #subcribers = new Map()
  #doStopListening = null

  constructor (database, routingFn) {
    /** routingFn
     * The database instance to index.
     * @type {Fireproof}
     */
    this.database = database
    this.#doStopListening = database.registerListener((changes) => this.#onChanges(changes))
    /**
     * The map function to apply to each entry in the database.
     * @type {Function}
     */
    this.routingFn =
      routingFn ||
      function (_, emit) {
        emit('*')
      }
    this.dbHead = null
  }

  /**
   * Subscribe to a topic emitted by the event function.
   * @param {string} topic - The topic to subscribe to.
   * @param {Function} subscriber - The function to call when the topic is emitted.
   * @returns {Function} A function to unsubscribe from the topic.
   * @memberof Listener
   * @instance
   */
  on (topic, subscriber, since) {
    const listOfTopicSubscribers = getTopicList(this.#subcribers, topic)
    listOfTopicSubscribers.push(subscriber)
    if (typeof since !== 'undefined') {
      this.database.changesSince(since).then(({ rows: changes }) => {
        const keys = topicsForChanges(changes, this.routingFn).get(topic)
        if (keys) keys.forEach((key) => subscriber(key))
      })
    }
    return () => {
      const index = listOfTopicSubscribers.indexOf(subscriber)
      if (index > -1) listOfTopicSubscribers.splice(index, 1)
    }
  }

  #onChanges (changes) {
    if (Array.isArray(changes)) {
      const seenTopics = topicsForChanges(changes, this.routingFn)
      for (const [topic, keys] of seenTopics) {
        const listOfTopicSubscribers = getTopicList(this.#subcribers, topic)
        listOfTopicSubscribers.forEach((subscriber) => keys.forEach((key) => subscriber(key)))
      }
    } else {
      // reset event
      if (changes.reset) {
        for (const [, listOfTopicSubscribers] of this.#subcribers) {
          listOfTopicSubscribers.forEach((subscriber) => subscriber(changes))
        }
      }
    }
    // if changes is special, notify all listeners?
    // first make the example app use listeners
  }
}

function getTopicList (subscribersMap, name) {
  let topicList = subscribersMap.get(name)
  if (!topicList) {
    topicList = []
    subscribersMap.set(name, topicList)
  }
  return topicList
}

// copied from src/db-index.js
const makeDoc = ({ key, value }) => ({ _id: key, ...value })

/**
 * Transforms a set of changes to events using an emitter function.
 *
 * @param {ChangeEvent[]} changes
 * @param {Function} routingFn
 * @returns {Array<string>} The topics emmitted by the event function.
 * @private
 */
const topicsForChanges = (changes, routingFn) => {
  const seenTopics = new Map()
  changes.forEach(({ key, value, del }) => {
    if (del || !value) value = { _deleted: true }
    routingFn(makeDoc({ key, value }), (t) => {
      const topicList = getTopicList(seenTopics, t)
      topicList.push(key)
    })
  })
  return seenTopics
}