UNPKG

@code-o-mat/history-cache

Version:

A data object structure that logs all changes using the immutable library

370 lines (314 loc) 10 kB
import Bunyan from 'bunyan'; const log = Bunyan.createLogger({name: "HistoryCache"}); import { List, Map } from 'immutable'; import { createMap } from 'immutable-map-symbol-preprocessor'; import uuid from 'uuid/v4'; import { NONE, PROPS } from '@code-o-mat/globals/lib/constants/values'; import { CACHE, HISTORY_CACHE, } from '@code-o-mat/globals/lib/constants/types'; import { assertIsObject, assertIsKeyOrNONE, assertIsStringOrNONE, assertIsIntOrNONE, } from '@code-o-mat/globals/lib/assertions/js-types'; import { assertIsSearchTerm } from './util-methods.js'; import { APPEND_TO_HISTORY, FIND_INDEX, TRIM, } from './constants/methods'; import Log from './log'; import Meta from '@code-o-mat/globals/lib/abstracts/meta'; class HistoryCache extends Meta { static FAMILY = CACHE; static TYPE = HISTORY_CACHE; FAMILY = CACHE; TYPE = HISTORY_CACHE; constructor(data = {}) { super(); assertIsObject(data, 'Invalid data object for Cache'); const dataMap = createMap(data); this[PROPS] = { data: dataMap, index: 0, limit: NONE, history: List(), eventIndex: Map(), tagIndex: Map(), UID: uuid(), UIDIndex: Map(), } this[APPEND_TO_HISTORY](dataMap, 'START', 'DEFAULT STATE'); } /** * get the UID of this historyCache. * @return {string} the UID */ uid() { return this[PROPS].UID; } /** * gets or sets the maximum limit for the history to be kept. * if no param, returs the limit, if an integer, * sets the limit and returns the HistoryCache. * @param {int|NONE} limit * @return {HistoryCache | int} */ limit(limit = NONE) { if (limit === NONE) return this[PROPS].limit; assertIsIntOrNONE(limit, 'Invalid limit set for history'); this[PROPS].limit = limit === -1 ? NONE : limit; return this; } /** * get the current data as a js object * @return {Object} */ data() { return this[PROPS].data.toJS(); } // ITEM OPERATIONS /** * Get a value by key. * @param {*} key * @return {*} value or NONE if not exists */ get(key = NONE, dflt = NONE) { assertIsKeyOrNONE(key); return this[PROPS].data.get(key, dflt); } /** * Get a value by key and version */ getFrom(key = NONE, term = NONE, dflt = NONE) { if (term === NONE) return this.get(key); assertIsKeyOrNONE(key); const version = this.version(term); if (version === NONE) return dflt; return version.data.get(key, dflt); } merge(newData) { assertIsObject(newData, `HistoryCache Invalid Object Error: Must merge JavaScript object or Immutable Map` ); if (Map.isMap(newData) === false) newData = createMap(newData); this[PROPS].data = this[PROPS].data.merge(newData); return this; } /** * Set a value at data[key] * Will create a new log entry. * @param {*} key * @param {*} value * @param {string|NONE} comment * @return {HistoryCache} this for continuation. */ set(key, value, comment = NONE) { assertIsKeyOrNONE(key); assertIsStringOrNONE(comment); const newData = this[PROPS].data.set(key, value); const valueStr = String(value).slice(0, 25); this[APPEND_TO_HISTORY](newData, `set ${key} = ${valueStr}`, comment); this[PROPS].data = newData; return this; } /** * checks out an earlier version of the cache if exists. * This will re-attach the earlier version to the * history as the latest... If the search term is not found in the history, * the current data state will be returned and no history will be affected. * Additionally, a warning will be thrown. * @param {*} term A search term that retrieve the history index via FIND_INDEX method. * @return {Object} object with the following params; * data: the recovered data * note: (if history not found, notes the attempt and its failure) * event: auto generated event for this checkout. * comment: the comment for this checkout * originalMeta: The event and comment from the data in its original history. * */ checkout(term, checkoutComment = NONE) { const index = this[FIND_INDEX](term); assertIsStringOrNONE(checkoutComment); const { history } = this[PROPS]; const checkoutEventNote = `Checkout of ${String(term)}`; if (index === NONE) { log.warn(`Ignoring checkout of a non-existent version.`); return { ..._history.get(-1), note: `${checkoutEventNote}: Not Found` } } const checkedOutData = history.get(index); const newDataItem = this[APPEND_TO_HISTORY]( createMap(checkedOutData), `checkout of ${String(term)} from history`, checkoutComment ); const { event, comment } = checkedOutData; return { ...checkedOutData, event: newDataItem.event, comment: newDataItem.comment, originalMeta: { event, comment } } } /** * get the revision index of the current data. * @return {int} the index */ index() { return this[PROPS].index; } tag(tag = NONE) { assertIsStringOrNONE(tag); if (tag === NONE) { log.warn(`Tried to set an invalid tag in HistoryCache. Tags must be a string`); return this; } const { history, tagIndex } = this[PROPS]; const lastHistoryItem = history.last(); this[PROPS].history = history.pop().push({...lastHistoryItem, tag}); this[PROPS].tagIndex = tagIndex.set(tag.slice(0, 100), this[PROPS].index); return this; } /** * retrieves the data from an earlier version. * but does not touch anything in the history * and does not reset the version. * @return { Map } */ version(term) { const index = this[FIND_INDEX](term); if (index === NONE) { log.warn('Tried to get an undefined HistoryCache version'); return NONE; } return this[PROPS].history.get(index); } /** * returns a Log object.. this will * get the log in form of .js object, JSON or a string * @return { Object | String } */ log(count) { return new Log(this[PROPS].history, count); } /** * Resets everything. Erases all history and resets data * to the original default state. * @return {[type]} [description] */ reset() { const defaultData = this[PROPS].history.get(0); this[PROPS].index = 0; this[APPEND_TO_HISTORY](defaultData, event = 'RESET', tag = 'RESET'); } /** * @function APPEND_TO_HISTORY * put a data state into the history. * @private * @param { Map } data * @param { string } event * @param { string } comment. * @return {Object} * data: the data appended. * event: the event named. * comment: the comment * */ [APPEND_TO_HISTORY](data, event = NONE, comment = NONE) { const { history, eventIndex, currentTag, UIDIndex, index, limit, } = this[PROPS]; if (comment === NONE) comment = `Revision: ${index}` const UID = uuid(); const date = new Date(); const historyDataItem = { date, UID, data, event, comment, tag: currentTag === NONE ? '' : currentTag }; const trimmed = this[TRIM]( index, limit, history.push(historyDataItem), eventIndex.set(event, index), UIDIndex.set(UID, index) ); this[PROPS].history = trimmed.history; this[PROPS].eventIndex = trimmed.eventIndex; this[PROPS].UIDIndex = trimmed.UIDIndex; this[PROPS].index++; this[PROPS].currentTag = NONE; return historyDataItem; } /** * @function FIND_INDEX * @description: Find a history index to match the search term. * @private * @param { * } term * @return { uint | NONE } the index that matches the search term */ [FIND_INDEX](term) { assertIsSearchTerm(term, `Invalid Search Term for History Cache ${String(term)}`); const { index } = this[PROPS]; if (Number.isInteger(term) === true && term > -1) { if (term > index - 1) { log.warn("Invalid index, returning most recent"); return index - 1; } return term; } const { eventIndex, tagIndex, UIDIndex } = this[PROPS]; let foundIndex = UIDIndex.get(term, NONE); if (foundIndex !== NONE) return foundIndex; foundIndex = tagIndex.get(term, NONE); if (foundIndex !== NONE) return foundIndex; return eventIndex.get(term, NONE); } /** * @function TRIM * @description If there is a history limit, will * trim a single entry from all history objects if that limit * is exceeded. * The earliest history object that occurs AFTER the default * setting is always the one deleted. The default settings are always * left intact. * @private * @return {Object} * history * historyEventIndex, * historyUidIndex */ [TRIM]( index, limit, history, eventIndex, UIDIndex, ) { if (limit === NONE || index < limit ) return { history, eventIndex, UIDIndex }; const dataToDelete = history.get(1); const event = dataToDelete.get('event'); const UID = dataToDelete.get('UID'); return { history: history.delete(1), eventIndex: eventIndex.delete(event), UIDIndex: UIDIndex.delete(UID), } } } export default HistoryCache;