config-file.js
1 const _ = require('underscore-plus'); 2 const fs = require('fs-plus'); 3 const dedent = require('dedent'); 4 const { Disposable, Emitter } = require('event-kit'); 5 const { watchPath } = require('./path-watcher'); 6 const CSON = require('season'); 7 const Path = require('path'); 8 const async = require('async'); 9 10 const EVENT_TYPES = new Set(['created', 'modified', 'renamed']); 11 12 module.exports = class ConfigFile { 13 static at(path) { 14 if (!this._known) { 15 this._known = new Map(); 16 } 17 18 const existing = this._known.get(path); 19 if (existing) { 20 return existing; 21 } 22 23 const created = new ConfigFile(path); 24 this._known.set(path, created); 25 return created; 26 } 27 28 constructor(path) { 29 this.path = path; 30 this.emitter = new Emitter(); 31 this.value = {}; 32 this.reloadCallbacks = []; 33 34 // Use a queue to prevent multiple concurrent write to the same file. 35 const writeQueue = async.queue((data, callback) => 36 CSON.writeFile(this.path, data, error => { 37 if (error) { 38 this.emitter.emit( 39 'did-error', 40 dedent` 41 Failed to write \`${Path.basename(this.path)}\`. 42 43 ${error.message} 44 ` 45 ); 46 } 47 callback(); 48 }) 49 ); 50 51 this.requestLoad = _.debounce(() => this.reload(), 200); 52 this.requestSave = _.debounce(data => writeQueue.push(data), 200); 53 } 54 55 get() { 56 return this.value; 57 } 58 59 update(value) { 60 return new Promise(resolve => { 61 this.requestSave(value); 62 this.reloadCallbacks.push(resolve); 63 }); 64 } 65 66 async watch(callback) { 67 if (!fs.existsSync(this.path)) { 68 fs.makeTreeSync(Path.dirname(this.path)); 69 CSON.writeFileSync(this.path, {}, { flag: 'wx' }); 70 } 71 72 await this.reload(); 73 74 try { 75 return await watchPath(this.path, {}, events => { 76 if (events.some(event => EVENT_TYPES.has(event.action))) 77 this.requestLoad(); 78 }); 79 } catch (error) { 80 this.emitter.emit( 81 'did-error', 82 dedent` 83 Unable to watch path: \`${Path.basename(this.path)}\`. 84 85 Make sure you have permissions to \`${this.path}\`. 86 On linux there are currently problems with watch sizes. 87 See [this document][watches] for more info. 88 89 [watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path\ 90 ` 91 ); 92 return new Disposable(); 93 } 94 } 95 96 onDidChange(callback) { 97 return this.emitter.on('did-change', callback); 98 } 99 100 onDidError(callback) { 101 return this.emitter.on('did-error', callback); 102 } 103 104 reload() { 105 return new Promise(resolve => { 106 CSON.readFile(this.path, (error, data) => { 107 if (error) { 108 this.emitter.emit( 109 'did-error', 110 `Failed to load \`${Path.basename(this.path)}\` - ${error.message}` 111 ); 112 } else { 113 this.value = data || {}; 114 this.emitter.emit('did-change', this.value); 115 116 for (const callback of this.reloadCallbacks) callback(); 117 this.reloadCallbacks.length = 0; 118 } 119 resolve(); 120 }); 121 }); 122 } 123 };