├── .gitignore ├── tests ├── test-instance.js └── test.js ├── package.json ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | npm-debug.log 3 | *.DS_Store 4 | node_modules 5 | tests/data.json 6 | tests/data.json.lock 7 | -------------------------------------------------------------------------------- /tests/test-instance.js: -------------------------------------------------------------------------------- 1 | // The simplest prettiest app: keeps track of how many 2 | // times it has been run. Magic! 3 | 4 | const data = require('../index.js')(); 5 | 6 | data.count = data.count || 0; 7 | data.count++; 8 | console.log('I have been run ' + data.count + ' times.'); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prettiest", 3 | "version": "1.1.0", 4 | "description": "Incredibly simple data storage and locking for command line scripts. Pairs nicely with shelljs and a nice chianti.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "cd tests && node test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/punkave/prettiest" 12 | }, 13 | "keywords": [ 14 | "lock", 15 | "database", 16 | "simple", 17 | "database", 18 | "shell", 19 | "scripts", 20 | "shelljs" 21 | ], 22 | "author": "P'unk Avenue LLC", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/punkave/prettiest/issues" 26 | }, 27 | "homepage": "https://github.com/punkave/prettiest", 28 | "dependencies": { 29 | "fs-ext": "^2.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/test.js: -------------------------------------------------------------------------------- 1 | const exec = require('child_process').exec; 2 | const fs = require('fs'); 3 | 4 | const instances = 100; 5 | 6 | let good = true; 7 | let terminated = 0; 8 | 9 | if (fs.existsSync('data.json')) { 10 | fs.unlinkSync('data.json'); 11 | } 12 | 13 | console.log('Starting ' + instances + ' instances simultaneously.\nThey may run in any order, but they will not\nrun simultaneously, and the final count in the database\nwill be ' + instances + '.\n'); 14 | 15 | for (let i = 0; (i < instances); i++) { 16 | exec('node test-instance', function(error, stdout, stderr) { 17 | terminated++; 18 | console.log(stdout); 19 | if (error) { 20 | console.error(stderr); 21 | good = false; 22 | } 23 | if (stderr.length) { 24 | console.error(stderr); 25 | good = false; 26 | } 27 | if (terminated === instances) { 28 | finish(); 29 | } 30 | }); 31 | }; 32 | 33 | function finish() { 34 | if (!good) { 35 | console.error('An error occurred. Tests failing.'); 36 | process.exit(1); 37 | } 38 | const data = JSON.parse(fs.readFileSync('data.json')); 39 | if (data.count != instances) { 40 | console.error('Execution count is wrong, locking bug (or you are testing on a filesystem that does not support flock)'); 41 | process.exit(1); 42 | } 43 | console.log('All tests passing.'); 44 | process.exit(0); 45 | } 46 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const flockSync = require('fs-ext').flockSync; 3 | const dirname = require('path').dirname; 4 | 5 | module.exports = function(options) { 6 | const dir = dirname(require.main.filename); 7 | options = options || {}; 8 | const dataFile = options.json || (dir + '/data.json'); 9 | const lockFile = dataFile + '.lock'; 10 | let data; 11 | let lockFd; 12 | 13 | // Prevent race conditions 14 | lock(); 15 | load(); 16 | process.on('exit', function() { 17 | save(); 18 | unlock(); 19 | }); 20 | 21 | return data; 22 | 23 | function lock() { 24 | lockFd = fs.openSync(lockFile, 'a'); 25 | flockSync(lockFd, 'ex'); 26 | } 27 | 28 | function unlock() { 29 | flockSync(lockFd, 'un'); 30 | try { 31 | fs.closeSync(lockFd); 32 | // We do NOT delete the lockfile. That can cause 33 | // race conditions. 34 | } catch (e) { 35 | // Another instance may have jumped on the file, 36 | // but as we've already unlocked, we don't 37 | // care anymore 38 | } 39 | } 40 | 41 | function load() { 42 | if (!fs.existsSync(dataFile)) { 43 | data = {}; 44 | return; 45 | } 46 | const content = fs.readFileSync(dataFile); 47 | data = JSON.parse(content); 48 | } 49 | 50 | function save() { 51 | // For data integrity, don't overwrite the old file until 52 | // we're sure it worked 53 | fs.writeFileSync(dataFile + '.tmp', JSON.stringify(data)); 54 | fs.renameSync(dataFile + '.tmp', dataFile); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prettiest 2 | 3 | ```javascript 4 | // The simplest script: keeps track of how many 5 | // times it has been run. It's like magic! 6 | 7 | var data = require('prettiest')(); 8 | 9 | data.count = data.count || 0; 10 | data.count++; 11 | console.log('I have been run ' + data.count + ' times.'); 12 | ``` 13 | 14 | `prettiest` provides simple command line apps with two important features: 15 | 16 | 1. **Persistent data.** Any changes made to the data object returned by `prettiest` are automatically saved when the app exits. 17 | 18 | 2. **Concurrency locks.** If two copies of your app start at the same time, the second one will always wait for the first one to finish before it proceeds. 19 | 20 | If you're replacing a shell script with something a little smarter, this module is a great companion for [shelljs](http://documentup.com/arturadib/shelljs). 21 | 22 | ## Install 23 | 24 | `npm install prettiest` 25 | 26 | ## Options 27 | 28 | You can specify where the JSON data file lives: 29 | 30 | ```javascript 31 | var data = require('prettiest')({ json: __dirname + '/mydatafile.json' }); 32 | ``` 33 | 34 | If you don't, the JSON file lives in the same directory with your app, and will be called `data.json`. 35 | 36 | `prettiest` also creates a lock file, which will have the same name as the JSON file, plus `.lock` at the end. To prevent race conditions, the lock file is not removed. Just leave it be. 37 | 38 | ## Caveats 39 | 40 | * The save-and-unlock behavior lives in a `process.on('exit')` handler. Which is great, actually, but just bear in mind it won't fire if node itself crashes. In which case your `data` object probably isn't ready to save anyway, right? 41 | 42 | * You don't want to use this in a web application. Duh. It's a simple, synchronous bit of magic for use in utilities with short execution times. 43 | 44 | * You don't want to use this in a super-long-running script, because it only saves your data to disk at the very end. It's meant for utilities that do a relatively simple job and then exit. 45 | 46 | ## Questions 47 | 48 | * "Can my code still be asynchronous?" Sure, knock yourself out. The save-and-unlock logic runs when your code exits. 49 | 50 | ## Credits 51 | 52 | `prettiest` was built for [ApostropheCMS](https://apostrophecms.com). 53 | 54 | ## Changelog 55 | 56 | 1.1.0: dependency on `fs-ext` bumped to 2.0.0, in hopes of smoother cross-platform compilation than with the prereleases. Moved to `const` and `let` since they are supported back to well before currently supported versions of node. 57 | 58 | 1.0.0: accepted pull request to use newer `fs-ext` because of compilation issues on newer systems. Thanks to Kerrick. Bumped to 1.0.0 stable. Now following semver. 59 | 60 | 0.1.0: initial release. 61 | 62 | --------------------------------------------------------------------------------