Skip to content

Commit

Permalink
Implement caching only where it's safe to do so
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 21e33c2 commit cc1f3d0
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 11 deletions.
29 changes: 20 additions & 9 deletions api/classes/Cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ const InvalidArgumentError = require('../error/InvalidArgumentError')
* of data
*/
class Cache {
/**
* A map containing the cache's index,
* mapping keys to cache entries
*
* @typedef {{
* status: 'pending' | undefined,
* promise: Promise | undefined,
* value: any
* }} Entry
*
* @type { Map.<String, Entry> }
*/
#index = new Map()
#maxEntries

Expand Down Expand Up @@ -60,21 +72,20 @@ class Cache {
}

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

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

/*
Expand All @@ -90,11 +101,11 @@ class Cache {
})

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

try {
const value = await provider()
this.#index.set(key, value)
this.#index.set(key, { value })
resolve(value)
} catch (e) {
reject(e)
Expand Down
11 changes: 11 additions & 0 deletions api/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,14 @@ function removeAllIntercepts (callee) {
}
}
exports.removeAllIntercepts = removeAllIntercepts

/**
* Check if there is a registered
* remote handler for an event
* @param { String } event
* @returns { Boolean }
*/
function hasRemoteHandler (event) {
return remoteHandlers.has(event)
}
exports.hasRemoteHandler = hasRemoteHandler
20 changes: 20 additions & 0 deletions api/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ const objectPath = require('object-path')
const state = require('./state')
const types = require('./types')
const client = require('./client')
const events = require('./events')
const random = require('./random')
const commands = require('./commands')
const variables = require('./variables')

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

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

const CACHE_MAX_ENTRIES = 10
const cache = new Cache(CACHE_MAX_ENTRIES)

/**
* Create a new id for an item
* that is unique and doesn't
Expand Down Expand Up @@ -95,6 +101,20 @@ exports.applyItem = applyItem
* @returns { Promise.<Item> }
*/
function getItem (id) {
/*
Use caching if it's safe to do so
The cache key must depend on the local state revision
in order to not get out of date, and that will only
get updated if the client is listening for the
'state.change' event
*/
if (
events.hasRemoteHandler('state.change') &&
state.getLocalRevision() !== 0
) {
return cache.cache(`${id}::${state.getLocalRevision()}`, () => commands.executeCommand('items.getItem', id))
}
return commands.executeCommand('items.getItem', id)
}
exports.getItem = getItem
Expand Down
25 changes: 23 additions & 2 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 CACHE_MAX_ENTRIES = 10
const cache = new Cache(CACHE_MAX_ENTRIES)

/**
* Keep a local
* copy of the state
Expand All @@ -31,6 +36,15 @@ let state
*/
let revision = 0

/**
* Get the local revision
* @returns { Number }
*/
function getLocalRevision () {
return revision
}
exports.getLocalRevision = getLocalRevision

/**
* Get the full remote state
* @returns { Promise.<any> }
Expand Down Expand Up @@ -117,13 +131,20 @@ 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
/*
If we can expect the revision to be updated,
use the caching layer to bundle calls together
*/
if (events.hasRemoteHandler('state.change') && revision !== 0) {
return cache.cache(`${path}::${revision}`, () => getRemoteState(path))
}
return getRemoteState(path)
}
}
exports.get = get
Expand Down

0 comments on commit cc1f3d0

Please sign in to comment.