altereagle/paperboy

View on GitHub
index.js

Summary

Maintainability
B
6 hrs
Test Coverage
// # Paperboy
// ### An intersystem communicator
// ![gif](https://media.giphy.com/media/eoUwc3wSwDOvK/giphy.gif)
// 
// **Example:** Paperboy can be used for data storage
// ```
// const Paperboy = require(`./library/paperboy`);
// const paperboy = new Paperboy({connectionName: `data-example`});
// paperboy.push(`example`, `Hello World!`);
// paperboy.pull(`example`)
//  .then((result) => {
//    console.log(result); // "Hello World!" is logged to the console
//  });
// ```

// **Example:** Paperboy can be used as a publish/subscribe service
// ```
// const Paperboy = require(`./library/paperboy`);
// const paperboy = new Paperboy({connectionName: `pubsub-example`});
// paperboy.on(`my-event`, (data) => {
//   console.log(data); // "Hello World!" is logged to the console
// });
//
// paperboy.trigger(`my-event`, `Hello World!`);
// ```
// ---

// Paperboy loads modules it depends on to operate
const { ioredis:Redis, 'generic-pool': genericPool, dotenv } = require(`./dependencies`);

// Paperboy adds the `.env file` variables to the process
dotenv.config();

// Paperboy loads the native event emitter module
const EventEmitter = require(`events`);
const { exec }     = require(`child_process`);

// Paperboy creates an event metter using the native event emitter module
class PaperboyEventEmitter extends EventEmitter {}
const paperboyEvent = new PaperboyEventEmitter();

// #### Paperboy can create a Redis connection pool
const createRedisConnectionPool = ({connectionName = `unnamed-connection`} = {}, poolType = `generic`) => {
  // **Scenario:** The system requests a Redis connection
  return genericPool.createPool({
    // **Given** the connection pool needs to create a new Redis connection
    create: function() {
      
      // **And** the connection sets its url and name
      const getUrlAndConnectionName = () => {
        return new Promise((resolve) => {
          resolve({
            connectionName: `@${connectionName}-${poolType}`,
            url           : process.env.PAPERBOY_REDIS_URL
          });
        });
      };
      
      // **And** the connection is made
      const createRedisConnection = ({connectionName, url}) => {
        return new Promise((resolve) => {
          const connection = new Redis(url, {connectionName, enableReadyCheck: true});
          resolve(connection);
        });
      };
      
      // **And** the connection can listen to messages
      const listenForMessages = (connection) => {
        return new Promise((resolve) => {
          connection.on(`message`, (channel, message) => {
            paperboyEvent.emit(channel, message);
          });
          
          resolve(connection);
        });
      };
      
      // **And** the connection can listen to errors
      const listenForErrors = (connection) => {
        return new Promise((resolve) => {
          let execRan = false;
          connection.on(`error`, ({address, port}) => {
            const errorMessage = `paperboy-communicator can't connect to ${address}:${port}`;
            console.error(errorMessage);
            
            // _Do not try to start redis if it has already been tried_
            if(execRan) return;
            execRan = true;
            
            // * Check if the address is local
            if(address === `127.0.0.1`) {
              const platform  = process.platform;
              let execCommand = `sudo service redis-server start`;
              
              if(platform === `win32`) return;
              if(platform === `darwin`) execCommand = `brew services start redis`;
              
              // * Start the local Redis server
              exec(execCommand, (err, stdout, stderr) => {
                if (err) {
                  console.error(`Could not start redis on ${address}:${port}`);
                  return;
                }
                
                if(stdout){
                  console.info(`paperboy-communicator stdout: ${stdout}`);
                }
                
                if(stderr){
                  console.info(`paperboy-communicator stderr: ${stderr}`);
                }
              });
            }
          });

          // * Continue if the connection is ready
          connection.on(`ready`, () => {
            resolve(connection);
          });
        });
      };
      
      // **And** the creation method will return the new connection
      return getUrlAndConnectionName()
        .then(createRedisConnection)
        .then(listenForMessages)
        .then(listenForErrors);
    },
    
    // **Given** the connection pool needs to destroy a connection
    destroy: function(connection) {
      // **Then** the connection pool will returns the connection _(it does not destroy it yet)_
      return Promise.resolve(connection);
    },
    // The connection pool has settings
  }, {
    // - The connection pool has a minimum number of connections set to `0`
    min: 0,
    
    // - The connection pool has a maximum number of connections set to `1`
    max: 1,
    
    // - The connection pool automatically creates one connection
    autostart: true
  });
};

// #### The Paperboy module is a class
module.exports = class Paperboy {
  
  // Paperboy creates pools of connections for `data` operations and `triggering`, and `subscribing` to events
  constructor(options = {}) {

    // Paperboy has `3` Redis connection pools
    // > Redis connections in subscriber mode can _not_ trigger events or modify data!
    // > [source](https://github.com/luin/ioredis)
    this.pool = {
      // - Paperboy has a connection pool for data operations
      data     : createRedisConnectionPool(options, `data`),
      
      // - Paperboy has a connection pool for triggering events
      trigger  : createRedisConnectionPool(options, `trigger`),
      
      // - Paperboy has a connection pool for subscribing to events
      subscribe: createRedisConnectionPool(options, `subscribe`)
    };

    return this;
  }

  // #### Paperboy can store data
  push(key, value, args, details) {
    return new Promise((resolve, reject) => {
      // - Data without a key is rejected
      if(!key) return reject(new Error(`no key`));

      // - Data without a value is rejected
      if(!value) return reject(new Error(`no value`));

      // Paperboy creates a copy of the data to send as a reply
      let reply = {};
      reply[key] = value;

      // Paperboy can release the `data` connection back into the [connection pool](#section-8)
      const releaseConnection = (connection) => this.pool.data.release(connection);

      // **Given** there are no special requirements to store the data
      if(!args && !details){
        // **When** a connection is acquired from the `data` connection pool
        return this.pool.data.acquire()
          .then((connection) => {
            // **Then** Paperboy will store the data
            connection.set(key, value)
              .then(() => {
                // **And** Paperboy will release the Redis data connection
                releaseConnection(connection);
                
                // **And** Paperboy will return a copy of the data that was stored
                resolve(reply);
              })
              .catch((error) => {
                // **But** Paperboy will release the connection when there are errors
                releaseConnection(connection);
                
                // **And** Paperboy will return the error that was caught
                reject(error);
              });
          })
          
          // Paperboy will reject pushed data if there is a problem acquiring a connection
          .catch(reject);
      }

      // **Given** there are special requirements to store the data
      this.pool.data.acquire()
        .then((connection) => {
          // **Then** Paperboy will store the data using the special requirements
          // > Special requirements in this case is an expiration time!
          // > [source](https://redis.io/commands/set)
          connection.set(key, value, args, details)
            .then(() => {
              releaseConnection(connection);
              resolve(reply);
            })
            .catch((error) => {
              releaseConnection(connection);
              reject(error);
            });
        })
        .catch(reject);
    });
  }

  // #### Paperboy can retrieve data
  // - The data can be retrieved by the name of the `key`
  pull(key) {
    // **Given** a connection is acquired from the `data` connection pool
    return this.pool.data.acquire()
      .then((connection) => {
        // **When** Paperboy retrieves the data by the name of the `key`
        return connection.get(key)
          .then((data) => {
            // **Then** Paperboy will release the connection into the pool
            this.pool.data.release(connection);
            // **And** Paperboy will return the data that was retrieved
            return data;
          })
          .catch((error) => {
            // **But** Paperboy will release the connection if there was an error
            this.pool.data.release(connection);
            // **And** Paperboy will return the error that was caught
            return error;
          });
      });
  }

  // #### Paperboy can remove data
  // - Data can be removed by the name of the `key`
  remove(key) {
    return new Promise((resolve, reject) => {
      // - Removal requests without a key are rejected
      if(!key) return reject(new Error(`no key`));
      
      // **Given** a connection is acquired from the `data` connection pool
      this.pool.data.acquire()
        .then((connection) => {
          // **When** Paperboy removes the data using the key
          return connection.del(key)
            .then((data) => {
              // **Then** Paperboy will release the connection into the pool
              this.pool.data.release(connection);
              
              // **And** Paperboy will return the data that was removed
              return data;
            })
            .then(resolve)
            .catch((error) => {
              // **But** Paperboy will release the connection if there was an error
              this.pool.data.release(connection);
              
              // **And** Paperboy will return the error that was caught
              reject(error);
            });
        })
        
        // Paperboy will reject deleted data if there is a problem acquiring a connection
        .catch(reject);
    });
  }

  // #### Paperboy can subscribe to an event once
  once(event, callback) {
    return new Promise((resolve, reject) => {
      // Paperboy can release `subscribe` connections
      const releaseConnection = (connection) => this.pool.subscribe.release(connection);
      
      // **Given** a connection is acquired from the `subscribe` connection pool
      this.pool.subscribe.acquire()
        .then((connection) => {

          // **And** Paperboy listens to the event once
          paperboyEvent.once(event, callback);

          // **Then** the connection subscribes to the event
          connection.subscribe(event, (error) => {
            // **But** Paperboy will reject the subscription request if there are errors
            if(error) return reject(error);
            
            // **And** Paperboy will resolve without errors if the subscription was made successfully
            resolve();
          });
          
          // Paperboy will always release the connection into the pool
          releaseConnection(connection);
        })
        
        // Paperboy will reject the subscription if there is a problem acquiring a connection
        .catch(reject);
    });
  }

  // #### Paperboy can subscribe to an event
  on(event, callback) {
    return new Promise((resolve, reject) => {
      // Paperboy can release `subscribe` connections
      const releaseConnection = (connection) => this.pool.subscribe.release(connection);
      
      // **Given** a connection is acquired from the `subscribe` connection pool
      this.pool.subscribe.acquire()
        .then((connection) => {

          // **And** Paperboy listens to the event
          paperboyEvent.on(event, callback);

          // **Then** the connection subscribes to the event
          connection.subscribe(event, (error) => {
            // **But** Paperboy will reject the subscription request if there are errors
            if(error) return reject(error);
            
            // **And** Paperboy will resolve without errors if the subscription was made successfully
            resolve();
          });
          
          // Paperboy will always release the connection into the pool
          releaseConnection(connection);
        })
        
        // Paperboy will reject the subscription if there is a problem acquiring a connection
        .catch(reject);
    });
  }

  // #### Paperboy can trigger an event
  trigger(event, data) {
    return new Promise((resolve, reject) => {
      // - Reject trigger calls that do not have an event 
      if(!event) return reject(new Error(`no event`));
      
      // **Given** a connection is acquired from the `trigger` connection pool
      this.pool.trigger.acquire()
        .then((connection) => {
          // **When** Paperboy publishes data for the event
          connection.publish(event, data)
            .then(() => {
              // **Then** Paperboy will release the connection into the pool
              this.pool.trigger.release(connection);
              
              // **And** Paperboy will return the data that was triggered
              resolve(data);
            })
            .catch((error) => {
              // **But** Paperboy will release the connection intot he pool if there was an error
              this.pool.trigger.release(connection);
              
              // **And** Paperboy will return the error that was caught
              reject(error);
            });
        })
        
        // Paperboy will reject the trigger request if there is a problem acquiring a connection
        .catch(reject);
    });
  }

  // #### Paperboy can remove an event listener
  removeListener(event, listener) {
    paperboyEvent.removeListener(event, listener);
  }

  // #### Paperboy can retrieve the number of listeners for an event
  listenerCount(event) {
    return paperboyEvent.listenerCount(event);
  }
};