date-store
Version:
Easily persist or get stored dates/times. Useful for conditionally making updates in an application based on the amount of time that has passed.
465 lines (415 loc) • 11.8 kB
JavaScript
'use strict';
const fs = require('fs');
const os = require('os');
const path = require('path');
const assert = require('assert');
const date = require('date.js');
const XDG_CONFIG_HOME = process.env.XDG_CONFIG_HOME;
/**
* Create a new `DateStore` with the given `name` and `options`.
*
* ```js
* const store = new DateStore();
* ```
*
* @param {String} `name` If `options.path` is supplied, `name` will be ignored. Otherwise `name` is used as the filename for the JSON store: `~/.date-store/{name}.json`
* @param {Object} `options` Optionally pass a `dir` and/or `path` to use for the JSON store. Default is `~/.date-store.json`
* @api public
*/
class DateStore {
constructor(name, options = {}) {
if (typeof name !== 'string') {
options = name || {};
name = options.name;
}
const { debounce = 5, indent = 2, home, base } = options;
this.name = name || 'date-store';
this.options = options;
this.debounce = debounce;
this.indent = indent;
this.home = home || XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
this.base = base || path.join(this.home, 'date-store');
this.path = this.options.path || path.join(this.base, `${this.name}.json`);
}
/**
* Store a `new Date()` for `key`.
*
* ```js
* store.set(key);
* ```
* @param {string} `key`
* @return {object} Returns the instance for chaining.
* @api public
*/
set(key) {
this.dates[key] = String(new Date());
this.save();
return this;
}
/**
* Get the stored date object for the given `key`
*
* ```js
* store.set('foo');
* console.log(store.get('foo'));
* //=> Mon Apr 11 2016 06:18:31 GMT-0400 (EDT)
*
* console.log(store.get('foo') instanceof Date);
* //=> true
* ```
*
* @param {String} `key` The name of the stored date to get.
* @return {Date} Returns the date object for `key`
* @api public
*/
get(key) {
if (this.has(key)) {
return (this.current = new Date(this.getRaw(key)));
}
}
/**
* Get the "raw" JSON-stringified date that was originally
* stored for `key`, or undefined if a stored value does not exist.
*
* ```js
* store.set('foo');
* console.log(store.getRaw('foo'));
* //=> Mon Apr 11 2016 08:39:10 GMT-0400 (EDT)
*
* console.log(store.getRaw('foo') instanceof Date);
* //=> false
*
* console.log(store.get('foo') instanceof Date);
* //=> true
* ```
*
* @param {String} `key` The name of the stored date to get.
* @return {String} Returns the stringified date for `key`
* @api public
*/
getRaw(key) {
return this.dates[key];
}
/**
* Get the numeric value corresponding to the time for stored date `key`,
* according to universal time. See the MDN docs for [.getTime][get-time].
*
* ```js
* store.set('foo');
* console.log(store.getTime('foo'));
* //=> 1460378350000
* ```
*
* @param {String} `key` The name of the stored date to get.
* @return {Number}
* @api public
*/
getTime(key) {
return this.get(key).getTime();
}
/**
* Return true if a date is stored for `key`, false if `undefined` or the
* stored value is invalid.
*
* ```js
* store.set('foo');
* console.log(store.has('foo'));
* //=> true
* ```
* @param {String} `key`
* @return {Boolean}
* @api public
*/
has(key) {
const date = this.dates[key];
return date && new Date(date).toString() !== 'Invalid Date';
}
/**
* Delete a date from the store.
*
* ```js
* store.del('foo');
* ```
* @param {String|Array} `key` Property name or array of property names.
* @return {Object} Returns the instance for chaining.
* @api public
*/
del(key) {
delete this.dates[key];
this.save();
return this;
}
/**
* Create a JavaScript date object from the given `str`. You may also supply
* an optional offset to the starting date. offset defaults to the current
* date and time. See [date.js][] for more details or to report date parsing
* related issues.
*
* ```js
* console.log(store.date('1 day from now'));
* //=> Tue Apr 12 2016 10:05:12 GMT-0400 (EDT)
* ```
* @param {String} `str` A human-readable string to pass to [date.js][]
* @return {Date} JavaScript Date object
* @api public
*/
date(...args) {
return date(...args);
}
/**
* Get the numeric value corresponding to the time for the date object returned
* from the [.date](#date) method.
*
* ```js
* console.log(store.date('1 day from now'));
* //=> Tue Apr 12 2016 10:05:12 GMT-0400 (EDT)
* ```
* @param {String} `timespan` A human-readable string to pass to [date.js][]
* @return {Date} JavaScript Date object
* @api public
*/
time(...args) {
return this.date(...args).getTime();
}
/**
* Returns the difference in seconds between stored date `key` and
* the date returned from calling [date.js][] on the given `timespan`.
*
* ```js
* console.log(store.diff('foo', '10 minutes ago'));
* //=> 338563
* ```
* @param {String} `key` The stored date to compare
* @param {String} `timespan` A human-readable string to pass to [date.js][]
* @return {Number} The difference in seconds between the two dates, or `NaN` if invalid.
* @api public
*/
diff(key, timespan) {
return this.getTime(key) - this.time(timespan);
}
/**
* Calls [.getTime](#getTime) and adds the result to the `.current` property,
* which is then used by other methods chained from `.lastSaved`.
*
* ```js
* store.set('bar');
* store.lastSaved('bar');
* console.log(store.current);
* //=> 1460378350000
* console.log(store.lastSaved('bar').moreThan('31 minutes ago'));
* //= false
* console.log(store.lastSaved('bar').lessThan('31 minutes ago'));
* //=> true
* ```
* @param {String} `key` The name of the stored date to set on `.current`
* @return {Object} Returns the instance for chaining.
* @api public
*/
lastSaved(key) {
this.current = this.getTime(key);
return this;
}
/**
* Calls [.time](#time) on the given `timespan`, and returns true if
* the returned time is older than `this.current`. _This method must
* be chained from [.lastSaved](#lastSaved)._
*
* ```js
* store.set('bar');
*
* console.log(store.lastSaved('bar').moreThan('31 minutes ago'));
* //= false
* console.log(store.lastSaved('bar').lessThan('1 minutes ago'));
* //=> true
* ```
* @param {String} `key` The name of the stored date to set on `.current`
* @return {Object} Returns the instance for chaining.
* @api public
*/
moreThan(timespan) {
return this.current && this.current < this.time(timespan);
}
isOlderThan(timespan) {
return this.current && this.current <= this.time(timespan);
}
isNewerThan(timespan) {
return this.current && this.current >= this.time(timespan);
}
/**
* Calls `.time()` on the given `timespan`, and returns true if
* the returned time is newer than `this.current`. _This method must
* be chained from [.lastSaved](#lastSaved)._
*
* ```js
* store.set('bar');
*
* console.log(store.lastSaved('bar').moreThan('31 minutes ago'));
* //= false
* console.log(store.lastSaved('bar').lessThan('1 minutes ago'));
* //=> true
* ```
* @param {String} `key` The name of the stored date to set on `.current`
* @return {Object} Returns the instance for chaining.
* @api public
*/
lessThan(timespan) {
return this.current && this.current > this.time(timespan);
}
/**
* Return an array of keys of items cached since the given `timespace`.
*
* ```js
* var keys = store.filterSince('1 week ago');
* ```
* @param {String} `timespan` A human-readable string to pass to [date.js][]
* @return {Array} Returns an array of keys
* @api public
*/
filterSince(timespan) {
const time = this.time(timespan);
const data = this.dates;
const res = [];
for (const key of Object.keys(data)) {
if (time < new Date(data[key]).getTime()) {
res.push(key);
}
}
return res;
}
/**
* Reset `store.dates` to an empty object.
*
* ```js
* store.clear();
* ```
* @name .clear
* @return {undefined}
* @api public
*/
clear() {
this.dates = {};
this.save();
}
/**
* Stringify the store. Takes the same arguments as `JSON.stringify`.
*
* ```js
* console.log(store.json(null, 2));
* ```
* @name .json
* @return {string}
* @api public
*/
json(replacer = null, space = this.indent) {
return JSON.stringify(this.dates, replacer, space);
}
/**
* Calls [.writeFile()](#writefile) to persist the store to the file system,
* after an optional [debounce](#options) period. This method should probably
* not be called directly as it's used internally by other methods.
*
* ```js
* store.save();
* ```
* @name .save
* @return {undefined}
* @api public
*/
save() {
if (!this.debounce) return this.writeFile();
if (this.save.debounce) return;
this.save.debounce = setTimeout(() => this.writeFile(), this.debounce);
}
/**
* Immediately write the store to the file system. This method should probably
* not be called directly. Unless you are familiar with the inner workings of
* the code it's recommended that you use .save() instead.
*
* ```js
* store.writeFile();
* ```
* @name .writeFile
* @return {undefined}
*/
writeFile() {
if (this.save.debounce) {
clearTimeout(this.save.debounce);
this.save.debounce = null;
}
if (!this.saved) mkdir(path.dirname(this.path), this.options.mkdir);
this.saved = true;
fs.writeFileSync(this.path, this.json(), { mode: 0o0600 });
}
/**
* Load the store.
* @return {object}
*/
load() {
try {
return (this._dates = JSON.parse(fs.readFileSync(this.path)));
} catch (err) {
if (err.code === 'EACCES') {
err.message += '\ndate-store does not have permission to load this file\n';
throw err;
}
if (err.code === 'ENOENT' || err.name === 'SyntaxError') {
this._dates = {};
return {};
}
}
}
/**
* Getter/setter for the `store.dates` object, which holds the values
* that are persisted to the file system.
*
* ```js
* console.log(store.dates); //=> {}
*
* store.set('foo');
* store.set('bar');
*
* console.log(store.dates);
* // { foo: 'Mon Apr 11 2016 08:39:10 GMT-0400 (EDT)',
* // bar: 'Mon Apr 11 2016 08:39:10 GMT-0400 (EDT)' }
* ```
* @name .dates
* @return {object}
* @api public
*/
set dates(val) {
this._dates = val;
this.save();
}
get dates() {
return this._dates || (this._dates = this.load());
}
}
/**
* Create a directory and any intermediate directories that might exist.
*/
function mkdir(dirname, options = {}) {
assert.equal(typeof dirname, 'string', 'expected dirname to be a string');
const opts = Object.assign({ cwd: process.cwd(), fs }, options);
const mode = opts.mode || 0o777 & ~process.umask();
const segs = path.relative(opts.cwd, dirname).split(path.sep);
const make = dir => fs.mkdirSync(dir, mode);
for (let i = 0; i <= segs.length; i++) {
try {
make((dirname = path.join(opts.cwd, ...segs.slice(0, i))));
} catch (err) {
handleError(dirname, opts)(err);
}
}
return dirname;
}
function handleError(dir, opts = {}) {
return (err) => {
if (err.code !== 'EEXIST' || path.dirname(dir) === dir || !opts.fs.statSync(dir).isDirectory()) {
throw err;
}
};
}
/**
* Expose `DateStore`
*/
module.exports = DateStore;