/ src / config-file.js
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  };