Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

steno

Package Overview
Dependencies
Maintainers
1
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

steno - npm Package Compare versions

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"
}
}
# 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)
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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc