diff --git a/server/service/rcon.js b/server/service/rcon.js index 9f34c08..b6601be 100644 --- a/server/service/rcon.js +++ b/server/service/rcon.js @@ -2,25 +2,25 @@ const { Rcon: { connect } } = require('rcon-client') const { readServerProperties } = require('../worlds/serverproperties') -const { deleteCachedItem, getCachedItem, setCachedItem } = require('../utils/cache') +const { cache } = require('../utils/cache') -const connection = 'connection' +const connectionCache = cache() exports.connect = async () => { - if (!getCachedItem(connection)) { + await connectionCache.store(async () => { const { 'rcon.port': port, 'rcon.password': password } = await readServerProperties() - setCachedItem(connection, await connect({ host: 'localhost', port, password })) - } + return await connect({ host: 'localhost', port, password }) + }) } exports.send = async request => { - if (!getCachedItem(connection)) { + const connection = connectionCache.read(() => { throw new Error('Not connected') - } + }) try { - return await getCachedItem(connection).send(request) + return await connection.send(request) } catch (error) { - deleteCachedItem(connection) + connectionCache.evict() throw error } } diff --git a/server/service/rcon.test.js b/server/service/rcon.test.js index c68d6cf..dc0c017 100644 --- a/server/service/rcon.test.js +++ b/server/service/rcon.test.js @@ -6,49 +6,70 @@ jest.mock('../utils/cache') const { Rcon: { connect: rconConnect } } = require('rcon-client') const { readServerProperties } = require('../worlds/serverproperties') -const { getCachedItem, setCachedItem } = require('../utils/cache') +const { cache } = require('../utils/cache') + +const connectionCache = { + store: jest.fn(), + read: jest.fn(), + evict: jest.fn() +} +cache.mockReturnValue(connectionCache) const { connect, send } = require('./rcon') -describe('send', () => { +describe('rcon', () => { const port = 123 const password = 'password' - const message = 'message' - const response = 'response' const connection = { send: jest.fn() } + const request = 'request' + const response = 'response' + const sendError = new Error('Send error') beforeEach(() => { - getCachedItem.mockReset() - setCachedItem.mockReset() + connectionCache.store.mockReset() + connectionCache.read.mockReset() + connectionCache.evict.mockReset() readServerProperties.mockReset() rconConnect.mockReset() connection.send.mockReset() }) it('should cache connection on connect', async () => { - readServerProperties - .mockResolvedValue({ 'rcon.port': port, 'rcon.password': password }) - rconConnect.mockReturnValue(connection) + connectionCache.store.mockImplementation(async create => { + await expect(create()).resolves.toBe(connection) + }) + readServerProperties.mockResolvedValue({ + 'rcon.port': port, + 'rcon.password': password + }) + rconConnect.mockResolvedValue(connection) await connect() + expect(readServerProperties).toHaveBeenCalled() expect(rconConnect).toHaveBeenCalledWith({ host: 'localhost', port, password }) - expect(setCachedItem).toHaveBeenCalledWith('connection', connection) }) - it('should not connect again', async () => { - getCachedItem.mockReturnValue(connection) + it('should send on cached connection', async () => { + connectionCache.read.mockImplementation(otherwise => { + expect(otherwise).toThrow(new Error('Not connected')) + return connection + }) + connection.send.mockResolvedValue(response) - await connect() + await expect(send(request)).resolves.toBe(response) - expect(readServerProperties).not.toHaveBeenCalled() - expect(rconConnect).not.toHaveBeenCalled() + expect(connection.send).toHaveBeenCalledWith(request) }) - it('should send on cached connection', async () => { - getCachedItem.mockReturnValue(connection) - connection.send.mockResolvedValue(response) + it('should evict connection on send error', async () => { + connectionCache.read.mockImplementation(otherwise => { + expect(otherwise).toThrow(new Error('Not connected')) + return connection + }) + connection.send.mockRejectedValue(sendError) - await expect(send(message)).resolves.toBe(response) + await expect(send(request)).rejects.toBe(sendError) - expect(connection.send).toHaveBeenCalledWith(message) + expect(connection.send).toHaveBeenCalledWith(request) + expect(connectionCache.evict).toHaveBeenCalled() }) }) diff --git a/server/utils/cache.js b/server/utils/cache.js index 3a14873..2fb430a 100644 --- a/server/utils/cache.js +++ b/server/utils/cache.js @@ -1,10 +1,14 @@ 'use strict' -const cache = {} - -exports.getCachedItem = key => cache[key] -exports.setCachedItem = (key, value) => { - cache[key] = value - return value +exports.cache = () => { + let storage = null + return { + store: async create => { + return storage || (storage = await create()) + }, + read: otherwise => storage || otherwise(), + evict: () => { + storage = null + } + } } -exports.deleteCachedItem = key => delete cache[key] diff --git a/server/utils/cache.test.js b/server/utils/cache.test.js index 82a75ad..0097795 100644 --- a/server/utils/cache.test.js +++ b/server/utils/cache.test.js @@ -1,19 +1,31 @@ 'use strict' -const { deleteCachedItem, getCachedItem, setCachedItem } = require('./cache') +const { cache } = require('./cache') describe('cache', () => { - const key = 'item' - const value = 'value' - it('should cache', () => { - expect(getCachedItem(key)).toBe(undefined) + it('should store, read and evict', async () => { + const myCache = cache() + const value = 'value' - expect(setCachedItem(key, value)).toBe(value) + const create = jest.fn() + create.mockResolvedValue(value) + await expect(myCache.store(create)).resolves.toBe(value) + await expect(myCache.store(create)).resolves.toBe(value) - expect(getCachedItem(key)).toBe(value) + expect(create).toHaveBeenCalledTimes(1) - deleteCachedItem(key) + const otherwise = jest.fn() + expect(myCache.read(otherwise)).toBe(value) - expect(getCachedItem(key)).toBe(undefined) + myCache.evict() + + otherwise.mockImplementation(() => { + throw new Error('no value') + }) + expect(() => myCache.read(otherwise)).toThrow(new Error('no value')) + + await expect(myCache.store(create)).resolves.toBe(value) + + expect(create).toHaveBeenCalledTimes(2) }) })