@oat-sa/tao-test-runner-qti
Version:
TAO Test Runner QTI implementation
219 lines (205 loc) • 7.37 kB
JavaScript
define(['lodash', 'core/store', 'taoQtiTest/runner/proxy/cache/itemPreloader'], function (_, store, itemPreloaderFactory) { 'use strict';
_ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _;
store = store && Object.prototype.hasOwnProperty.call(store, 'default') ? store['default'] : store;
itemPreloaderFactory = itemPreloaderFactory && Object.prototype.hasOwnProperty.call(itemPreloaderFactory, 'default') ? itemPreloaderFactory['default'] : itemPreloaderFactory;
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2017-2021 Open Assessment Technologies SA
*/
/**
* The default number of items to store
*/
const defaultConfig = {
itemTTL: 0,
maxSize: 10,
preload: false
};
/**
* Create an item store
* @param {object} [options]
* @param {number} [options.itemTTL = 0] - The TTL for each item in the store, in milliseconds. 0 means no TTL.
* @param {number} [options.maxSize = 10] - the store limit
* @param {boolean} [options.preload] - do we preload items when storing them
* @param {string} [options.testId] - the unique identifier of the test instance, required if preload is true
*
* @returns {itemStore}
*/
function itemStoreFactory(options) {
const config = _.defaults(options || {}, defaultConfig);
// in memory storage
const getStore = () => store('item-cache', store.backends.memory);
// maintain an index to resolve existence synchronously
const index = new Map();
let lastIndexedPosition = 0;
// check if a key has expired
const isExpired = key => {
const meta = index.get(key);
if (meta) {
return config.itemTTL && Date.now() - meta.timestamp >= config.itemTTL;
}
return false;
};
// retrieve the first item by position from the index
const findFirstIndexedItem = () => {
let first = null;
let lowest = Number.POSITIVE_INFINITY;
index.forEach((item, key) => {
if (item.position < lowest) {
lowest = item.position;
first = key;
}
});
return first;
};
// retrieve all expired items from the index
const findExpiredItems = () => {
const expired = [];
index.forEach((item, key) => {
if (isExpired(key)) {
expired.push(key);
}
});
return expired;
};
let itemPreloader;
if (config.preload) {
itemPreloader = itemPreloaderFactory(_.pick(config, ['testId']));
}
/**
* @typedef itemStore
*/
return {
/**
* Setter to override the cache size
*
* @param {number} cacheSize
*/
setCacheSize(cacheSize) {
config.maxSize = cacheSize;
},
/**
* Sets the item store TTL.
* @param {number} ttl
*/
setItemTTL(ttl) {
config.itemTTL = ttl;
},
/**
* Get the item form the given key/id/uri
* @param {string} key - something identifier
* @returns {Promise<Object>} the item
*/
get(key) {
if (!this.has(key)) {
return Promise.resolve();
}
return getStore().then(itemStorage => itemStorage.getItem(key));
},
/**
* Check whether the given item is in the store
* @param {string} key - something identifier
* @returns {boolean}
*/
has(key) {
return index.has(key) && !isExpired(key);
},
/**
* Add/Set an item into the store, under the given key
* @param {string} key - something identifier
* @param {object} item - the item
* @returns {Promise<boolean>} chains
*/
set(key, item) {
return getStore().then(itemStorage => {
return itemStorage.setItem(key, item).then(updated => {
if (updated) {
if (!index.has(key)) {
index.set(key, {
position: lastIndexedPosition++,
timestamp: Date.now()
});
}
if (config.preload) {
_.defer(() => itemPreloader.preload(item));
}
}
//do we reach the limit ? then remove one
if (index.size > 1 && index.size > config.maxSize) {
return this.remove(findFirstIndexedItem()).then(removed => updated && removed);
}
return updated;
});
});
},
/**
* Update some data of a store item
* @param {string} key - something identifier
* @param {string} updateKey - key to update
* @param {*} updateValue - new data for the updateKey
* @returns {Promise<boolean>} resolves with the update status
*/
update(key, updateKey, updateValue) {
if (index.has(key) && _.isString(updateKey)) {
return getStore().then(itemStorage => {
return itemStorage.getItem(key).then(itemData => {
if (_.isPlainObject(itemData)) {
itemData[updateKey] = updateValue;
return itemStorage.setItem(key, itemData);
}
});
});
}
return Promise.resolve(false);
},
/**
* Remove the item from the store
* @param {string} key - something identifier
* @returns {Promise<boolean>} resolves once removed
*/
remove(key) {
if (index.has(key)) {
return getStore().then(itemStorage => {
index.delete(key);
return itemStorage.getItem(key).then(item => {
if (config.preload) {
_.defer(() => itemPreloader.unload(item));
}
}).then(() => itemStorage.removeItem(key));
});
}
return Promise.resolve(false);
},
/**
* Prune the store from expired content.
* @returns {Promise}
*/
prune() {
return Promise.all(findExpiredItems().map(this.remove));
},
/**
* Clear the store
* @returns {Promise}
*/
clear() {
return getStore().then(itemStorage => {
index.clear();
return itemStorage.clear();
});
}
};
}
return itemStoreFactory;
});