You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

318 lines
6.5 KiB

'use strict'
const fs = require('fs')
const EventEmitter = require('events')
const flatstr = require('flatstr')
const inherits = require('util').inherits
const BUSY_WRITE_TIMEOUT = 100
const sleep = require('atomic-sleep')
// 16 MB - magic number
// This constant ensures that SonicBoom only needs
// 32 MB of free memory to run. In case of having 1GB+
// of data to write, this prevents an out of memory
// condition.
const MAX_WRITE = 16 * 1024 * 1024
function openFile (file, sonic) {
sonic._opening = true
sonic._writing = true
sonic.file = file
fs.open(file, 'a', (err, fd) => {
if (err) {
sonic.emit('error', err)
return
}
sonic.fd = fd
sonic._reopening = false
sonic._opening = false
sonic._writing = false
sonic.emit('ready')
if (sonic._reopening) {
return
}
// start
var len = sonic._buf.length
if (len > 0 && len > sonic.minLength && !sonic.destroyed) {
actualWrite(sonic)
}
})
}
function SonicBoom (fd, minLength, sync) {
if (!(this instanceof SonicBoom)) {
return new SonicBoom(fd, minLength, sync)
}
this._buf = ''
this.fd = -1
this._writing = false
this._writingBuf = ''
this._ending = false
this._reopening = false
this._asyncDrainScheduled = false
this.file = null
this.destroyed = false
this.sync = sync || false
this.minLength = minLength || 0
if (typeof fd === 'number') {
this.fd = fd
process.nextTick(() => this.emit('ready'))
} else if (typeof fd === 'string') {
openFile(fd, this)
} else {
throw new Error('SonicBoom supports only file descriptors and files')
}
this.release = (err, n) => {
if (err) {
if (err.code === 'EAGAIN') {
if (this.sync) {
// This error code should not happen in sync mode, because it is
// not using the underlining operating system asynchronous functions.
// However it happens, and so we handle it.
// Ref: https://github.com/pinojs/pino/issues/783
try {
sleep(BUSY_WRITE_TIMEOUT)
this.release(undefined, 0)
} catch (err) {
this.release(err)
}
} else {
// Let's give the destination some time to process the chunk.
setTimeout(() => {
fs.write(this.fd, this._writingBuf, 'utf8', this.release)
}, BUSY_WRITE_TIMEOUT)
}
} else {
this.emit('error', err)
}
return
}
if (this._writingBuf.length !== n) {
this._writingBuf = this._writingBuf.slice(n)
if (this.sync) {
try {
do {
n = fs.writeSync(this.fd, this._writingBuf, 'utf8')
this._writingBuf = this._writingBuf.slice(n)
} while (this._writingBuf.length !== 0)
} catch (err) {
this.release(err)
return
}
} else {
fs.write(this.fd, this._writingBuf, 'utf8', this.release)
return
}
}
this._writingBuf = ''
if (this.destroyed) {
return
}
var len = this._buf.length
if (this._reopening) {
this._writing = false
this._reopening = false
this.reopen()
} else if (len > 0 && len > this.minLength) {
actualWrite(this)
} else if (this._ending) {
if (len > 0) {
actualWrite(this)
} else {
this._writing = false
actualClose(this)
}
} else {
this._writing = false
if (this.sync) {
if (!this._asyncDrainScheduled) {
this._asyncDrainScheduled = true
process.nextTick(emitDrain, this)
}
} else {
this.emit('drain')
}
}
}
}
function emitDrain (sonic) {
sonic._asyncDrainScheduled = false
sonic.emit('drain')
}
inherits(SonicBoom, EventEmitter)
SonicBoom.prototype.write = function (data) {
if (this.destroyed) {
throw new Error('SonicBoom destroyed')
}
this._buf += data
var len = this._buf.length
if (!this._writing && len > this.minLength) {
actualWrite(this)
}
return len < 16384
}
SonicBoom.prototype.flush = function () {
if (this.destroyed) {
throw new Error('SonicBoom destroyed')
}
if (this._writing || this.minLength <= 0) {
return
}
actualWrite(this)
}
SonicBoom.prototype.reopen = function (file) {
if (this.destroyed) {
throw new Error('SonicBoom destroyed')
}
if (this._opening) {
this.once('ready', () => {
this.reopen(file)
})
return
}
if (this._ending) {
return
}
if (!this.file) {
throw new Error('Unable to reopen a file descriptor, you must pass a file to SonicBoom')
}
this._reopening = true
if (this._writing) {
return
}
fs.close(this.fd, (err) => {
if (err) {
return this.emit('error', err)
}
})
openFile(file || this.file, this)
}
SonicBoom.prototype.end = function () {
if (this.destroyed) {
throw new Error('SonicBoom destroyed')
}
if (this._opening) {
this.once('ready', () => {
this.end()
})
return
}
if (this._ending) {
return
}
this._ending = true
if (!this._writing && this._buf.length > 0 && this.fd >= 0) {
actualWrite(this)
return
}
if (this._writing) {
return
}
actualClose(this)
}
SonicBoom.prototype.flushSync = function () {
if (this.destroyed) {
throw new Error('SonicBoom destroyed')
}
if (this.fd < 0) {
throw new Error('sonic boom is not ready yet')
}
if (this._buf.length > 0) {
fs.writeSync(this.fd, this._buf, 'utf8')
this._buf = ''
}
}
SonicBoom.prototype.destroy = function () {
if (this.destroyed) {
return
}
actualClose(this)
}
function actualWrite (sonic) {
sonic._writing = true
var buf = sonic._buf
var release = sonic.release
if (buf.length > MAX_WRITE) {
buf = buf.slice(0, MAX_WRITE)
sonic._buf = sonic._buf.slice(MAX_WRITE)
} else {
sonic._buf = ''
}
flatstr(buf)
sonic._writingBuf = buf
if (sonic.sync) {
try {
var written = fs.writeSync(sonic.fd, buf, 'utf8')
release(null, written)
} catch (err) {
release(err)
}
} else {
fs.write(sonic.fd, buf, 'utf8', release)
}
}
function actualClose (sonic) {
if (sonic.fd === -1) {
sonic.once('ready', actualClose.bind(null, sonic))
return
}
// TODO write a test to check if we are not leaking fds
fs.close(sonic.fd, (err) => {
if (err) {
sonic.emit('error', err)
return
}
if (sonic._ending && !sonic._writing) {
sonic.emit('finish')
}
sonic.emit('close')
})
sonic.destroyed = true
sonic._buf = ''
}
module.exports = SonicBoom