I am working on the queue which requirements are:
- We can 3 types of events (start, heartbeat which will be added to the queue in 5sec intervals, and the end event)
- Backup the events in local storage for cases like (close browser, close tab)
- Send only the newest event if we have more than 1 event in the stack
- 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