Skip to content
On this page

a barebones localfile-backed storage for stubbing out without redis or RDMS

read/write JSON ​

N.B. Don't put any cycles in your .db; this is just using JSON.stringify, so cycles will make it unsavable.

const jsonLoad = require('@ashnazg/dumbfile').load; // ~/projects/ashnazg-npm/dumbfile/json.js

var handle = await jsonLoad('./store.json');
console.log(handle.db); // contents of file are now ready on .db

handle.db.property = 'foo'; // mutate .db, it's just a plain old JS object.
await handle.save(); // saved back to disk, in a human-friendly format. Using tabs.

// admin edited config file while app was running?
await handle.reload(); // .db has been wiped and replaced with file's contents again.

Example: persisting session tokens through a server reboot ​

var seven_days = 1000*60*60*24*7;

var session_db_ready = dumb.load('../tokens.json').then(sys => { // N.B. if you've got src/ watched with something like nodemon, make sure the state file isn't in the watch list.
	// create schemas if missing
	sys.db = Object.assign({
		tokens: {},
	}, sys.db);
	sys.setSessionExpiration = function(user, lifespan = seven_days) {
		var token = genToken(user, lifespan);
		var expires_on = Date.now() + lifespan;
		this.db.tokens[user] = this.db.tokens[user] || {}; // this could be the first token creation for this user
		this.db.tokens[user][token] = expires_on;
		this.save(); // firing and forgetting the 'save completed' promise; I don't need confirmation that this hit disk for just sessions.
		console.log("new db", this.db);
		return token;
	};
	sys.isValidSession = function(user, token) {
		var my_tokens = this.db.tokens[user];
		if (!my_tokens) return false;
		var expiration_date = my_tokens[token];
		if (expiration_date === undefined) return false;
		if (expiration_date > Date.now()) return getUid(user);
		delete my_tokens[token];
		return false;
	};
	return sys;
}

// caller should provide exactly one of pass or token
async function checkAuth(user, pass, token) {
	if (pass) {
		var uid = checkPass(user, pass);
		if (uid) {
			var sys = await session_db_ready;
			var token = sys.setSessionExpiration(user);
			return {uid, token};
		}
	} else if (token) {
		var sys = await session_db_ready;
		uid = sys.isValidSession(user, token);
		if (uid) {
			return {uid, token};
		}
		throw {message: 'token expired'};
	}
	throw {message: 'no acceptable credential provided'};
}

Not using JSON? ​

There's a no-format-assumptions underlying layer you can use. It does speak only utf8, as I only use raw Buffers when I have to.

var loadFile = require('./utf8.js').createDumbFile;

var handle = loadFile('./store.raw');
var data = await handle.load();
await handle.save('new data');

It also has a synchronous save function:

handle.saveSync('new data');

handling not-yet-existing files ​

Both of the handle creation functions have two more params: a default value that's returned when the file doesn't exist, and a chmod mode used during save()'s file creation.

var handle = loadFile(filename, 'default config lines', 0o660); // default chmod os owner r/w only; 660 says group also has r/w perms.
var handle = jsonLoad(filename, {default: 'setting'}, 0o660);

Release 1.1.4 ​

Turned off some dbg logging I didn't mean to commit

Release 1.1.3 ​

json file management no longer depends on {this.db} for anything -- this allows me to pass a subtree to a submodule, along with the save func, and now each submodule can share dumb state file without knowing they're not the root.

Release 1.1.2 ​

Now treats empty files the same way it treats non-existent files: init the state to the specified defaults.

Release 1.1.1 ​

1.1.0 has a bug where multiple parallel calls to async save can result in the OS seeing overlapping writes during one "open for truncate and write" file handle.

So now, a horde of async save()s results in all but the first save leaving a note for the first save job to handle it, and then waiting for it to finally give the "latest version has been persisted."

JavaScript/Bash code released under the MIT License.