Comparing version 0.3.2 to 0.4.0
86
index.js
@@ -1,2 +0,2 @@ | ||
var fs = require('fs') | ||
var fs = require('graceful-fs') | ||
var path = require('path') | ||
@@ -6,57 +6,69 @@ | ||
function Writer(filename) { | ||
this.filename = filename | ||
this._callbacks = [] | ||
// Returns a temporary file | ||
// Example: for /some/file will return /some/.~file | ||
function getTempFile(file) { | ||
return path.join(path.dirname(file), '.~' + path.basename(file)) | ||
} | ||
Writer.prototype._callback = function(err, data, next) { | ||
if (err) throw err | ||
next() | ||
function Writer(file) { | ||
this.file = file | ||
this.callbacks = [] | ||
} | ||
Writer.prototype.setCallback = function(cb) { | ||
this._callback = cb | ||
return this | ||
} | ||
Writer.prototype.write = function(data, cb) { | ||
// Save callback for later | ||
this.callbacks.push(cb) | ||
if (this.lock) { | ||
// File is locked | ||
// Save data for later | ||
this.next = data | ||
if (cb) this._callbacks.push(cb) | ||
} else { | ||
// File is not locked | ||
// Lock it | ||
this.lock = true | ||
var self = this | ||
fs.writeFile(this.filename, data, function(err) { | ||
// Write data to a temporary file | ||
var tmpFile = getTempFile(this.file) | ||
fs.writeFile(tmpFile, data, function(err) { | ||
function next() { | ||
self.lock = false | ||
if (self.next) { | ||
var data = self.next | ||
self.next = null | ||
self.write(data) | ||
} | ||
if (err) { | ||
// On error, call all the callbacks and return | ||
while (c = this.callbacks.shift()) c(err) | ||
return | ||
} | ||
self._callback(err, data, next) | ||
// On success rename the temporary file to the real file | ||
fs.rename(tmpFile, this.file, function(err) { | ||
var c | ||
while (c = self._callbacks.shift()) { | ||
c(err) | ||
} | ||
if (cb) cb(err) | ||
}) | ||
// Call all the callbacks | ||
while (c = this.callbacks.shift()) c(err) | ||
// Unlock file | ||
this.lock = false | ||
// Write next data if any | ||
if (this.next) { | ||
var data = this.next | ||
this.next = null | ||
this.write(data) | ||
} | ||
}.bind(this)) | ||
}.bind(this)) | ||
} | ||
return this | ||
} | ||
module.exports = function(filename) { | ||
filename = path.resolve(filename) | ||
return writers[filename] = writers[filename] || new Writer(filename) | ||
module.exports.writeFile = function(file, data, cb) { | ||
// Convert to absolute path | ||
file = path.resolve(file) | ||
// Create or get writer | ||
writers[file] = writers[file] || new Writer(file) | ||
// Write | ||
writers[file].write(data, cb) | ||
} |
{ | ||
"name": "steno", | ||
"version": "0.3.2", | ||
"description": "Fast non-blocking file writer for Node", | ||
"version": "0.4.0", | ||
"description": "Simple file writer with race condition prevention and atomic writing", | ||
"main": "index.js", | ||
@@ -19,5 +19,6 @@ "scripts": { | ||
"asynchronous", | ||
"synchronous", | ||
"race", | ||
"condition", | ||
"atomic", | ||
"writing", | ||
"safe" | ||
@@ -32,6 +33,10 @@ ], | ||
"devDependencies": { | ||
"after": "^0.8.1", | ||
"husky": "^0.6.2", | ||
"tap-dot": "^0.2.3", | ||
"tape": "^3.0.1" | ||
}, | ||
"dependencies": { | ||
"graceful-fs": "^3.0.8" | ||
} | ||
} |
113
README.md
# steno [![](https://badge.fury.io/js/steno.svg)](http://badge.fury.io/js/steno) [![](https://travis-ci.org/typicode/steno.svg?branch=master)](https://travis-ci.org/typicode/steno) | ||
> Fast and safe file writer that prevents race condition | ||
> Simple file writer with __race condition prevention__ and __atomic writing__. | ||
```javascript | ||
var steno = require('steno') | ||
steno('file.txt').write('data') | ||
``` | ||
Built on [graceful-fs](https://github.com/isaacs/node-graceful-fs) and used in [lowdb](https://github.com/typicode/lowdb). | ||
## Example | ||
## Without steno | ||
If you need to write to file, you either use `writeFileSync` or `writeFile`. The first is blocking and the second doesn't prevent race condition. | ||
Let's say you have a server and want to save data to disk: | ||
For example: | ||
```javascript | ||
// Very slow but file's content will always be 10000 | ||
for (var i = 0; i <= 10000; i++) { | ||
fs.writeFileSync('file.txt', i) | ||
} | ||
``` | ||
var data = { counter: 0 }; | ||
```javascript | ||
// Very fast but file's content may be 5896, 2563, 9856, ... | ||
for (var i = 0; i <= 10000; i++) { | ||
fs.writeFile('file.txt', i, function() {}) | ||
} | ||
``` | ||
server.post('/', function (req, res) { | ||
++data.counter; | ||
With steno: | ||
```javascript | ||
// Very fast and file's content will always be 10000 | ||
for (var i = 0; i <= 10000; i++) { | ||
steno('file.txt').write(i) | ||
} | ||
fs.writeFile('data.json', JSON.stringify(obj), function (err) { | ||
if (err) throw err; | ||
res.end(); | ||
}); | ||
}) | ||
``` | ||
Race condition is prevented and it runs in `2ms` versus `~5500ms` with `fs.writeFileSync`. | ||
Now if you have many requests, for example `1000`, there's a risk that you end up with: | ||
## How it works | ||
```javascript | ||
steno('file.txt').write('A') // starts writing A to file | ||
steno('file.txt').write('B') // still writing A, B is buffered | ||
steno('file.txt').write('C') // still writing A, B is replaced by C | ||
// ... | ||
// A has been written to file | ||
// starts writting C (B has been skipped) | ||
``` | ||
// In your server | ||
data.counter === 1000; | ||
When file is being written, data is stored in memory and flushed to disk as soon as possible. Please note also that steno skips intermediate data (B in this example) and assumes to be run in a single process. | ||
## Methods | ||
__steno(filename)__ | ||
Returns writer for filename. | ||
__writer.write(data, [cb])__ | ||
Writes data to file. If file is already being written, data is buffered until it can be written. | ||
```javascript | ||
steno('file.txt').write('data') | ||
// In data.json | ||
data.counter === 865; // ... or any other value | ||
``` | ||
An optional callback can be set to be notified when data has been flushed. | ||
Why? Because, `fs.write` doesn't guarantee that the call order will be kept. Also, if the server is killed while `data.json` is being written, the file can get corrupted. | ||
```javascript | ||
function w(data) { | ||
steno('file.txt').write(data, function(err) { | ||
if (err) throw err | ||
console.log('OK') | ||
}) | ||
} | ||
## With steno | ||
w('A') | ||
w('B') | ||
w('C') | ||
// OK | ||
// OK | ||
// OK | ||
``` | ||
__writer.setCallback(cb)__ | ||
Sets a writer level callback that is called __only__ after file has been written. Useful for creating atomic writers, logging, delaying, ... | ||
```javascript | ||
var atomicWriter = steno('tmp.txt').setCallback(function(err, data, next) { | ||
if (err) throw err | ||
fs.rename('tmp.txt', 'file.txt', function(err) { | ||
if (err) throw err | ||
console.log('OK') | ||
next() | ||
server.post('/increment', function (req, res) { | ||
++obj.counter | ||
steno.writeFile('data.json', JSON.stringify(obj), function (err) { | ||
if (err) throw err; | ||
res.end(); | ||
}) | ||
}) | ||
``` | ||
atomicWriter.write('A') | ||
atomicWriter.write('B') | ||
atomicWriter.write('C') | ||
With steno you'll always have the same data in your server and file. And in case of a crash, file integrity will be preserved. | ||
// OK | ||
// OK | ||
__Important__: works only in a single instance of Node. | ||
// File has been actually written twice | ||
``` | ||
## License | ||
MIT - [Typicode](https://github.com/typicode) |
78
test.js
var fs = require('fs') | ||
var path = require('path') | ||
var after = require('after') | ||
var test = require('tape') | ||
@@ -7,82 +8,49 @@ var steno = require('./') | ||
function reset() { | ||
if (fs.existsSync('.~tmp.txt')) fs.unlinkSync('.~tmp.txt') | ||
if (fs.existsSync('tmp.txt')) fs.unlinkSync('tmp.txt') | ||
} | ||
var max = 10 * 1000 * 1000 | ||
var writer = steno('tmp.txt') | ||
var max = 1000 | ||
test('writer without callback', function(t) { | ||
test('There should be a race condition with fs', function (t) { | ||
reset() | ||
t.plan(1) | ||
setTimeout(function() { | ||
t.equal(+fs.readFileSync('tmp.txt'), max) | ||
}, 1000) | ||
var next = after(max, function () { | ||
t.notEqual(+fs.readFileSync('tmp.txt'), max) | ||
}) | ||
for (var i= 0; i <= max; i++) { | ||
writer.write(i) | ||
for (var i= 0; i < max; ++i) { | ||
fs.writeFile('tmp.txt', i, function (err) { | ||
if (err) throw err | ||
next() | ||
}) | ||
} | ||
}) | ||
test('writer default callback', function(t) { | ||
test('There should not be a race condition with steno', function(t) { | ||
reset() | ||
t.plan(2) | ||
// default callback should call function next | ||
// when err is null | ||
var err = null | ||
var next = function() { | ||
t.pass('next was called') | ||
} | ||
writer._callback(err, '', next) | ||
// default callback should throw an error | ||
t.throws(function() { | ||
writer._callback(new Error()) | ||
}) | ||
}) | ||
test('writer with callback', function(t) { | ||
reset() | ||
t.plan(1) | ||
writer.setCallback(function(err, data, next) { | ||
if (data === max) { | ||
t.equal(+fs.readFileSync('tmp.txt'), max) | ||
} | ||
next() | ||
var next = after(max, function () { | ||
t.notEqual(+fs.readFileSync('tmp.txt'), max) | ||
}) | ||
for (var i= 0; i <= max; i++) { | ||
writer.write(i) | ||
for (var i= 0; i < max; ++i) { | ||
steno.writeFile('tmp.txt', i, function (err) { | ||
if (err) throw er | ||
next() | ||
}) | ||
} | ||
}) | ||
test('writer error with callback', function(t) { | ||
test('Error handling with steno', function(t) { | ||
reset() | ||
t.plan(1) | ||
var writer = steno(__dirname + '/dir/doesnt/exist') | ||
var file = __dirname + '/dir/doesnt/exist' | ||
writer.setCallback(function(err) { | ||
steno.writeFile(file, '', function(err) { | ||
t.equal(err.code, 'ENOENT') | ||
}) | ||
writer.write('') | ||
}) | ||
test('write callback', function(t) { | ||
reset() | ||
t.plan(3) | ||
writer.write('A', t.false) | ||
writer.write('B', t.false) | ||
writer.write('C', t.false) | ||
}) | ||
test('store absolute paths', function(t) { | ||
reset() | ||
t.plan(1) | ||
t.equal(writer, steno(path.resolve('tmp.txt'))) | ||
}) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1
6173
1
4
7
99
55
+ Addedgraceful-fs@^3.0.8
+ Addedgraceful-fs@3.0.12(transitive)
+ Addednatives@1.1.6(transitive)