Skip to content

Commit

Permalink
Add a local cache to the state.get and items.getItem functions
Browse files Browse the repository at this point in the history
Signed-off-by: Axel Boberg <[email protected]>
  • Loading branch information
axelboberg committed Mar 24, 2024
1 parent 40af571 commit c72b6a3
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 4 deletions.
107 changes: 107 additions & 0 deletions api/classes/Cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: 2024 Sveriges Television AB
//
// SPDX-License-Identifier: MIT

const InvalidArgumentError = require('../error/InvalidArgumentError')

/**
* @class Cache
* @description A simple in-memory cache
* that can store any type
* of data
*/
class Cache {
#index = new Map()
#maxEntries

constructor (maxEntries = 10) {
this.#maxEntries = maxEntries
}

/**
* Prepare the cache index for insertion,
* this will make sure that the index
* stays within the set max size
*/
#prepareIndexForInsertion () {
if (this.#index.size < this.#maxEntries) {
return
}
const firstKey = this.#index.keys().next().value
this.#index.delete(firstKey)
}

/**
* Cache the response of a provider function,
* the response will be returned if there's
* a valid entry in the cache index
*
* If there's a cache hit,
* the provider function
* will not be called
*
* If the cache is waiting for the provider
* while a new request is made, a promise will
* be returned to the first request value,
* avoiding multiple simultaneous requests
* for the same data
*
* @param { String } key
* @param { Function.<Promise.<any>> } provider
* @returns { Promise.<any> }
*/
async cache (key, provider) {
if (typeof key !== 'string') {
throw new InvalidArgumentError('The provided key must be a string')
}

if (typeof provider !== 'function') {
throw new InvalidArgumentError('The provided provider must be a function')
}

if (this.#index.has(key)) {
const value = this.#index.get(key)

/*
If there is a pending promise for the value,
return that rather than starting a new request
*/
if (
typeof value === 'object' &&
value.__status === 'pending' &&
value.__promise
) {
return value.__promise
}
return this.#index.get(key)
}

/*
Create a promise that can be used
to return the fetched value to all
waiting recipients
*/
let resolve
let reject
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve
reject = _reject
})

this.#prepareIndexForInsertion()
this.#index.set(key, { __status: 'pending', __promise: promise })

try {
const value = await provider()
this.#index.set(key, value)
resolve(value)
} catch (e) {
reject(e)
this.#index.delete(key)
}

return promise
}
}

module.exports = Cache
9 changes: 8 additions & 1 deletion api/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ const variables = require('./variables')
const MissingArgumentError = require('./error/MissingArgumentError')
const InvalidArgumentError = require('./error/InvalidArgumentError')

const Cache = require('./classes/Cache')

const ITEM_CACHE_MAX_ENTRIES = 10
const itemCache = new Cache(ITEM_CACHE_MAX_ENTRIES)

/**
* Create a new id for an item
* that is unique and doesn't
Expand Down Expand Up @@ -95,7 +100,9 @@ exports.applyItem = applyItem
* @returns { Promise.<Item> }
*/
function getItem (id) {
return commands.executeCommand('items.getItem', id)
return itemCache.cache(`${id}::${state.getCurrentRevision()}`, () => {
return commands.executeCommand('items.getItem', id)
})
}
exports.getItem = getItem

Expand Down
17 changes: 14 additions & 3 deletions api/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ const merge = require('../shared/merge')
const commands = require('./commands')
const events = require('./events')

const Cache = require('./classes/Cache')

const STATE_CACHE_MAX_ENTRIES = 10
const stateCache = new Cache(STATE_CACHE_MAX_ENTRIES)

/**
* Keep a local
* copy of the state
Expand All @@ -24,6 +29,13 @@ let state
*/
let revision = 0

/**
* Get the current local
* revision of the state
* @returns { Number }
*/
exports.getCurrentRevision = () => revision

/**
* Get the full remote state
* @returns { Promise.<any> }
Expand Down Expand Up @@ -110,14 +122,13 @@ exports.apply = apply
* @returns { Promise.<State> }
*/
async function get (path) {
const newState = await getRemoteState(path)

if (!path) {
const newState = await getRemoteState()
revision = newState._revision
state = newState
return state
} else {
return newState
return stateCache.cache(`${path}::${revision}`, () => getRemoteState(path))
}
}
exports.get = get
Expand Down

0 comments on commit c72b6a3

Please sign in to comment.