@code-o-mat/history-cache
Version:
A data object structure that logs all changes using the immutable library
370 lines (314 loc) • 10 kB
JavaScript
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;