const _ = require('underscore-plus') const fs = require('fs-plus') const dedent = require('dedent') const {Emitter} = require('event-kit') const {watchPath} = require('./path-watcher') const CSON = require('season') const Path = require('path') const async = require('async') const temp = require('temp') const EVENT_TYPES = new Set([ 'created', 'modified', 'renamed' ]) module.exports = class ConfigFile { static at (path) { if (!this._known) { this._known = new Map() } const existing = this._known.get(path) if (existing) { return existing } const created = new ConfigFile(path) this._known.set(path, created) return created } constructor (path) { this.path = path this.emitter = new Emitter() this.value = {} this.reloadCallbacks = [] // Use a queue to prevent multiple concurrent write to the same file. const writeQueue = async.queue((data, callback) => { (async () => { try { await writeCSONFileAtomically(this.path, data) } catch (error) { this.emitter.emit('did-error', dedent ` Failed to write \`${Path.basename(this.path)}\`. ${error.message} `) } callback() })() }) this.requestLoad = _.debounce(() => this.reload(), 200) this.requestSave = _.debounce((data) => writeQueue.push(data), 200) } get () { return this.value } update (value) { return new Promise(resolve => { this.requestSave(value) this.reloadCallbacks.push(resolve) }) } async watch (callback) { if (!fs.existsSync(this.path)) { fs.makeTreeSync(Path.dirname(this.path)) CSON.writeFileSync(this.path, {}, {flag: 'wx'}) } await this.reload() try { const watcher = await watchPath(this.path, {}, events => { if (events.some(event => EVENT_TYPES.has(event.action))) this.requestLoad() }) return watcher } catch (error) { this.emitter.emit('did-error', dedent ` Unable to watch path: \`${Path.basename(this.path)}\`. Make sure you have permissions to \`${this.path}\`. On linux there are currently problems with watch sizes. See [this document][watches] for more info. [watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path\ `) } } onDidChange (callback) { return this.emitter.on('did-change', callback) } onDidError (callback) { return this.emitter.on('did-error', callback) } reload () { return new Promise(resolve => { CSON.readFile(this.path, (error, data) => { if (error) { this.emitter.emit('did-error', `Failed to load \`${Path.basename(this.path)}\` - ${error.message}`) } else { this.value = data || {} this.emitter.emit('did-change', this.value) for (const callback of this.reloadCallbacks) callback() this.reloadCallbacks.length = 0 } resolve() }) }) } } function writeCSONFile (path, data) { return new Promise((resolve, reject) => { CSON.writeFile(path, data, error => { if (error) reject(error) else resolve() }) }) } async function writeCSONFileAtomically (path, data) { const tempPath = temp.path() await writeCSONFile(tempPath, data) await rename(tempPath, path) } function rename (oldPath, newPath) { return new Promise((resolve, reject) => { fs.rename(oldPath, newPath, error => { if (error) reject(error) else resolve() }) }) }