0
\$\begingroup\$

I am working on the queue which requirements are:

  1. We can 3 types of events (start, heartbeat which will be added to the queue in 5sec intervals, and the end event)
  2. Backup the events in local storage for cases like (close browser, close tab)
  3. Send only the newest event if we have more than 1 event in the stack
  4. Retry sending with exponential backoff on error

The event schema type looks like this:

type BillableEventData = {
  event: string, // "connectionStart", "connectionHeartBeat", "connectionEnd"
  timestampStart?: number, // only exists in the connectionStart
  timestampEnd?: number, // only exists in connectionHeartBeat and connectionEnd
  shortId: string,
  connectionId: string,
  customerIdParticipant: number,
}

My queue looks like this:

import APIClient, { BillableEventData } from '../../apiClient'

export class EventsQueue {
  events: BillableEventData[]
  sendingLocked = false

  constructor () {
    this.events = JSON.parse(localStorage.getItem('bevents') || '[]')
    this.sendNextEventFromTheStack = this.sendNextEventFromTheStack.bind(this)
    this.sendEventToAPI = this.sendEventToAPI.bind(this)
  }

  enqueueEvent (event: BillableEventData) {
    this.events.push(event)
    localStorage.setItem('bevents', JSON.stringify(this.events))

    this.sendNextEventFromTheStack()
  }

  async sendEventToAPI (event: BillableEventData) {
    this.sendingLocked = true

    try {

      // this is custom method based on axios, for retires the axios-retry is used
      await APIClient.postBillableEvent([event])

      const eventsCopy: BillableEventData[] = this.events
        .filter(
          ev => {
            if (ev.timestampEnd) {
              if (!event.timestampEnd) return true
              return ev.timestampEnd !== event.timestampEnd
            }

            if (ev.timestampStart) {
              if (!event.timestampStart) return true
              return ev.timestampStart !== event.timestampStart
            }
          }
        )
        .filter((ev) => ev.event === 'connectionHeartBeat')
        .sort((ev1, ev2) => {
          if (ev1.timestampEnd && ev2.timestampEnd) {
            return ev2.timestampEnd - ev1.timestampEnd
          }
          return 0
        })

      const arrayWithLastEvent = eventsCopy.length > 0 ? [eventsCopy[0]] : []

      const combinedArray = arrayWithLastEvent.concat(
        this.events
          .filter(
            ev => {
              if (ev.timestampEnd) {
                if (!event.timestampEnd) return true
                return ev.timestampEnd !== event.timestampEnd
              }

              if (ev.timestampStart) {
                if (!event.timestampStart) return true
                return ev.timestampStart !== event.timestampStart
              }
            }
          )
          .filter((ev) => ev.event !== 'connectionHeartBeat')
      )

      localStorage.setItem('bevents', JSON.stringify(combinedArray))
      this.events = combinedArray

      this.sendingLocked = false
      this.sendNextEventFromTheStack()
    } catch (e) {
      if (this.events.length > 1) {
        const eventsCopy: BillableEventData[] = this.events
          .filter(
            (ev) => {
              if (ev.timestampEnd) {
                if (!event.timestampEnd) return true
                return ev.timestampEnd !== event.timestampEnd
              }

              if (ev.timestampStart) {
                if (!event.timestampStart) return true
                return ev.timestampStart !== event.timestampStart
              }
            }
          )
          .filter((ev) => ev.event === 'connectionHeartBeat')
          .sort((ev1, ev2) => {
            if (ev1.timestampEnd && ev2.timestampEnd) {
              return ev2.timestampEnd - ev1.timestampEnd
            }
            return 0
          })

        const arrayWithLastEvent = eventsCopy.length > 0 ? [eventsCopy[0]] : []

        const combinedArray = arrayWithLastEvent.concat(
          this.events
            .filter(
              ev => {
              if (ev.timestampEnd) {
                if (!event.timestampEnd) return true
                return ev.timestampEnd !== event.timestampEnd
              }

              if (ev.timestampStart) {
                if (!event.timestampStart) return true
                return ev.timestampStart !== event.timestampStart
              }
            })
            .filter((ev) => ev.event !== 'connectionHeartBeat')
        )

        localStorage.setItem('bevents', JSON.stringify(combinedArray))
        this.events = combinedArray
      }

      // timeout as a delay when for example API is down
      setTimeout(() => {
        this.sendingLocked = false
        this.sendNextEventFromTheStack()
      }, 2000)
    }
  }

  sendNextEventFromTheStack () {
    if (!this.events.length) {
      return console.log('[EventsQueue] Queue has been stopped.')
    }

    if (this.sendingLocked) {
      return console.log('[EventsQueue] Queue is currently in progress.')
    }

    const event = this.events[0]
    this.sendEventToAPI(event)
  }
}

The entire queue with its stack is created by making the new instance and restoring the stack from the localstorage.

Is there any room for improvement? Does the lock flag avoid race conditions since the events will be enqueued in 5sec intervals (heartbeats) and for example connection end will be enqueued at the same time as the last heartbeat?

Many thanks for your all feedback

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.