<commit_before>const Data = require('./data');
const EventEmitter = require('events');

class Block extends EventEmitter {
	constructor(board, $block, config) {
		super();
		this.board = board;
		this.$block = $block;
		this.x = config.x;
		this.y = config.y;
		this.rotate = config.rotate;
		this.config = config;
		this.inputQueues = {
			top: [],
			left: [],
			right: [],
			bottom: [],
		};
		this.outputQueues = {
			top: [],
			left: [],
			right: [],
			bottom: [],
		};
	}

	get center() {
		return {
			x: (this.x + .5) * 50,
			y: (this.y + .5) * 50,
		};
	}

	get top() {
		return {
			x: (this.x + .5) * 50,
			y: this.y * 50,
		};
	}

	get left() {
		return {
			x: this.x * 50,
			y: (this.y + .5) * 50,
		};
	}

	get right() {
		return {
			x: (this.x + 1) * 50,
			y: (this.y + .5) * 50,
		};
	}

	get bottom() {
		return {
			x: (this.x + .5) * 50,
			y: (this.y + 1) * 50,
		};
	}

	rotatedDirection(direction) {
		if (!this.config.rotate_levels) {
			return direction;
		}

		const directions = ['top', 'right', 'bottom', 'left'];
		return directions[(directions.indexOf(direction) + this.rotate) % 4];
	}

	passTo(direction, data) {
		if (direction === 'bottom' && this.y + 1 === this.board.height && this.x === this.board.outputBlockX) {
			this.board.output(data.value);
			data.fadeOut();
			return;
		}

		switch (direction){
			case 'top':
				if (0 <= this.y-1) {
					this.board.getBlock(this.x,this.y-1).input('bottom', data);
				} else {
					data.fadeOut();
				}
				break;
			case 'bottom':
				if (this.board.height > this.y+1) {
					this.board.getBlock(this.x,this.y+1).input('top', data);
				} else {
					data.fadeOut();
				}
				break;
			case 'left':
				if (0 <= this.x-1) {
					this.board.getBlock(this.x-1,this.y).input('right', data);
				} else {
					data.fadeOut();
				}
				break;
			case 'right':
				if (this.board.width > this.x+1) {
					this.board.getBlock(this.x+1,this.y).input('left', data);
				} else {
					data.fadeOut();
				}
				break;
			default:
				break;
		}

	}

	input(position, data) {
		this.inputQueues[position].push(data);
	}

	step() {
		if (this.config.type === 'empty') {
			// Erase all data passed to the empty block
			for (let source in this.inputQueues) {
				const queue = this.inputQueues[source];
				while (queue.length) {
					const data = queue.shift();
					this.emit('erase', data);
				}
			}
		} else if (this.config.type === 'wire') {
			const rotatedPlugs = this.config.io.plugs.map((direction) => this.rotatedDirection(direction));

			for (let source in this.queues) {
				const queue = this.queues[source];

				// When data exists in pluged direction
				if (queue.length !== 0 && rotatedPlugs.includes(source)) {
					const destinations = rotatedPlugs.filter((direction) => direction !== source);
					const data = queue.shift();

					// pass through
					const input = {};
					input[source] = data;

					const output = {};
					destinations.forEach((direction) => {
						const data = new Data(this.board, data.value);
						outputQueues[direction].push(data);
						output[direction] = data;
					});

					this.emit('pass', {in: input, out: output});
				}

				// Erase data when data exists in non-pluged direction
				if (!rotatedPlugs.includes(source)) {
					while (queue.length) {
						const data = queue.shift();
						this.emit('erase', data);
					}
				}
			}
		} else if (this.config.type === 'calc') {
			const rotatedPlugs = this.config.io.plugs.map((direction) => this.rotatedDirection(direction));

			for (let source in this.queues) {
				const queue = this.queues[source];

				// When data exists in pluged direction
				if (queue.length !== 0 && rotatedPlugs.includes(source)) {
					const destinations = rotatedPlugs.filter((direction) => direction !== source);
					const data = queue.shift();

					// Calculate and pass through
					const input = {};
					input[source] = data;

					const output = {};
					destinations.forEach((direction) => {
						const data = new Data(this.board, this.config.func(data.value));
						outputQueues[direction].push(data);
						output[direction] = data;
					});

					this.emit('pass', {in: input, out: output});
				}

				// Erase data when data exists in non-pluged direction
				if (!rotatedPlugs.includes(source)) {
					while (queue.length) {
						const data = queue.shift();
						this.emit('erase', data);
					}
				}
			}
		} else if (this.config.type === 'calc2') {
			const sources = this.config.io.in.map((direction) => this.rotatedDirection(direction));
			const destination = this.rotatedDirection(this.config.io.out);

			// Execute calculation when all inputs are satisfied
			if (sources.every((source) => this.queues[source].length > 0)) {
				const datas = [];

				// Calculate and pass through
				const input = {};
				sources.forEach((source, index) => {
					const data = this.queues[source].shift();
					input[source] = data;
					datas.push(data);
				});

				const output = {};

				const outData = new Data(this.board, this.config.func(...datas.map((data) => data.value)));
				outputQueues[direction].push(outData);
				output[direction] = outData;

				this.emit('pass', {in: input, out: output});
			}

			// Erase data when data exists in non-pluged direction
			for (let source in this.queues) {
				if (!rotatedIns.includes(source)) {
					let queue = this.queues[source];
					while (queue.length) {
						const data = queue.shift();
						data.fadeOut();
					}
				}
			}
		} else if (this.config.type === 'calc-switch') {
			const sources = this.config.io.in.map((direction) => this.rotatedDirection(direction));
			const destinations = this.config.io.out.map((direction) => this.rotatedDirection(direction));

			// Execute calculation when all inputs are satisfied
			if (sources.every((source) => this.queues[source].length > 0)) {
				const datas = [];

				// Calculate and pass through
				const input = {};
				sources.forEach((source, index) => {
					const data = this.queues[source].shift();
					input[source] = data;
					datas.push(data);
				});

				const [directionIndex, value] = this.config.func(...datas.map((data) => data.value));

				const output = {};
				const data = new Data(this.board, this.config.func(value));
				const destination = destinations[directionIndex];
				outputQueues[direction].push(data);
				output[direction] = data;

				this.emit('pass', {in: input, out: output});
			}

			// Erase data when data exists in non-pluged direction
			for (let source in this.queues) {
				if (!rotatedIns.includes(source)) {
					const queue = this.queues[source];
					while (queue.length) {
						const data = queue.shift();
						data.fadeOut();
					}
				}
			}
		}
	}
}

module.exports = Block;
<commit_msg>Implement Block::pass method<commit_after>const Data = require('./data');
const EventEmitter = require('events');

class Block extends EventEmitter {
	constructor(board, $block, config) {
		super();
		this.board = board;
		this.$block = $block;
		this.x = config.x;
		this.y = config.y;
		this.rotate = config.rotate;
		this.config = config;
		this.inputQueues = {
			top: [],
			left: [],
			right: [],
			bottom: [],
		};
		this.outputQueues = {
			top: [],
			left: [],
			right: [],
			bottom: [],
		};
	}

	get center() {
		return {
			x: (this.x + .5) * 50,
			y: (this.y + .5) * 50,
		};
	}

	get top() {
		return {
			x: (this.x + .5) * 50,
			y: this.y * 50,
		};
	}

	get left() {
		return {
			x: this.x * 50,
			y: (this.y + .5) * 50,
		};
	}

	get right() {
		return {
			x: (this.x + 1) * 50,
			y: (this.y + .5) * 50,
		};
	}

	get bottom() {
		return {
			x: (this.x + .5) * 50,
			y: (this.y + 1) * 50,
		};
	}

	rotatedDirection(direction) {
		if (!this.config.rotate_levels) {
			return direction;
		}

		const directions = ['top', 'right', 'bottom', 'left'];
		return directions[(directions.indexOf(direction) + this.rotate) % 4];
	}

	input(position, data) {
		this.inputQueues[position].push(data);
	}

	step() {
		if (this.config.type === 'empty') {
			// Erase all data passed to the empty block
			for (let source in this.inputQueues) {
				const queue = this.inputQueues[source];
				while (queue.length) {
					const data = queue.shift();
					this.emit('erase', data);
				}
			}
		} else if (this.config.type === 'wire') {
			const rotatedPlugs = this.config.io.plugs.map((direction) => this.rotatedDirection(direction));

			for (let source in this.inputQueues) {
				const queue = this.inputQueues[source];

				// When data exists in pluged direction
				if (queue.length !== 0 && rotatedPlugs.includes(source)) {
					const destinations = rotatedPlugs.filter((direction) => direction !== source);
					const data = queue.shift();

					// pass through
					const input = {};
					input[source] = data;

					const output = {};
					destinations.forEach((direction) => {
						const data = new Data(this.board, data.value);
						outputQueues[direction].push(data);
						output[direction] = data;
					});

					this.emit('pass', {in: input, out: output});
				}

				// Erase data when data exists in non-pluged direction
				if (!rotatedPlugs.includes(source)) {
					while (queue.length) {
						const data = queue.shift();
						this.emit('erase', data);
					}
				}
			}
		} else if (this.config.type === 'calc') {
			const rotatedPlugs = this.config.io.plugs.map((direction) => this.rotatedDirection(direction));

			for (let source in this.inputQueues) {
				const queue = this.inputQueues[source];

				// When data exists in pluged direction
				if (queue.length !== 0 && rotatedPlugs.includes(source)) {
					const destinations = rotatedPlugs.filter((direction) => direction !== source);
					const data = queue.shift();

					// Calculate and pass through
					const input = {};
					input[source] = data;

					const output = {};
					destinations.forEach((direction) => {
						const data = new Data(this.board, this.config.func(data.value));
						outputQueues[direction].push(data);
						output[direction] = data;
					});

					this.emit('pass', {in: input, out: output});
				}

				// Erase data when data exists in non-pluged direction
				if (!rotatedPlugs.includes(source)) {
					while (queue.length) {
						const data = queue.shift();
						this.emit('erase', data);
					}
				}
			}
		} else if (this.config.type === 'calc2') {
			const sources = this.config.io.in.map((direction) => this.rotatedDirection(direction));
			const destination = this.rotatedDirection(this.config.io.out);

			// Execute calculation when all inputs are satisfied
			if (sources.every((source) => this.inputQueues[source].length > 0)) {
				const datas = [];

				// Calculate and pass through
				const input = {};
				sources.forEach((source, index) => {
					const data = this.inputQueues[source].shift();
					input[source] = data;
					datas.push(data);
				});

				const output = {};

				const outData = new Data(this.board, this.config.func(...datas.map((data) => data.value)));
				outputQueues[direction].push(outData);
				output[direction] = outData;

				this.emit('pass', {in: input, out: output});
			}

			// Erase data when data exists in non-pluged direction
			for (let source in this.inputQueues) {
				if (!rotatedIns.includes(source)) {
					let queue = this.inputQueues[source];
					while (queue.length) {
						const data = queue.shift();
						this.emit('erase', data);
					}
				}
			}
		} else if (this.config.type === 'calc-switch') {
			const sources = this.config.io.in.map((direction) => this.rotatedDirection(direction));
			const destinations = this.config.io.out.map((direction) => this.rotatedDirection(direction));

			// Execute calculation when all inputs are satisfied
			if (sources.every((source) => this.inputQueues[source].length > 0)) {
				const datas = [];

				// Calculate and pass through
				const input = {};
				sources.forEach((source, index) => {
					const data = this.inputQueues[source].shift();
					input[source] = data;
					datas.push(data);
				});

				const [directionIndex, value] = this.config.func(...datas.map((data) => data.value));

				const output = {};
				const data = new Data(this.board, this.config.func(value));
				const destination = destinations[directionIndex];
				outputQueues[direction].push(data);
				output[direction] = data;

				this.emit('pass', {in: input, out: output});
			}

			// Erase data when data exists in non-pluged direction
			for (let source in this.inputQueues) {
				if (!rotatedIns.includes(source)) {
					const queue = this.inputQueues[source];
					while (queue.length) {
						const data = queue.shift();
						this.emit('erase', data);
					}
				}
			}
		}
	}

	pass() {
		for (let direction in this.outputQueues) {
			const queue = this.outputQueues[direction];
			while (queue.length) {
				const data = queue.shift();
				this.passTo(direction, data);
			}
		}
	}

	passTo(direction, data) {
		if (direction === 'bottom' && this.y + 1 === this.board.height && this.x === this.board.outputBlockX) {
			this.board.output(data.value);
			data.fadeOut();
			return;
		}

		switch (direction){
			case 'top':
				if (0 <= this.y - 1) {
					this.board.getBlock(this.x, this.y - 1).input('bottom', data);
				} else {
					this.emit('erase', data);
				}
				break;
			case 'bottom':
				if (this.board.height > this.y + 1) {
					this.board.getBlock(this.x, this.y + 1).input('top', data);
				} else {
					this.emit('erase', data);
				}
				break;
			case 'left':
				if (0 <= this.x-1) {
					this.board.getBlock(this.x - 1, this.y).input('right', data);
				} else {
					this.emit('erase', data);
				}
				break;
			case 'right':
				if (this.board.width > this.x + 1) {
					this.board.getBlock(this.x + 1, this.y).input('left', data);
				} else {
					this.emit('erase', data);
				}
				break;
			default:
				break;
		}
	}
}

module.exports = Block;
<|endoftext|>


<commit_before><commit_msg>Bulkwork done<commit_after><|endoftext|>


<commit_before>'use strict';

module.exports = function(option, log, path, context, middleware, urls, template) {

    var //---------------
        //    Imports
        //---------------

        qfs = require('q-io/fs'),
        q = require('mercenary/promises'),
        lang = require('mercenary/lang'),
        constant = lang.constant,
        compose = lang.compose,
        destructured = lang.destructured,
        isArray = lang.is.array,
        isString = lang.is.string,

        //---------------
        //    Options
        //---------------

        overwrite = option('overwrite') !== false,
        verbose = option('verbose') || false,

        //----------------------
        //    Implementation
        //----------------------

        // Raise an error using the fail log.
        raise = function(error) {
            log.fail(error.stack || 'An unexpected error occurred');
        },

        // Promise to fill in missing directories for the given build path.
        mkdirs = function(build) {
            var dirPath = path.path.dirname(build);

            return (
                qfs.exists(dirPath)
                .then(function(exists) {
                    if (exists) {
                        return (
                            qfs.stat(dirPath)
                            .then(function(stat) {
                                if (!stat.isDirectory()) {
                                    throw new Error(
                                        'The path "' +
                                        dirPath +
                                        '" is not a directory');
                                }
                            }));
                    } else {
                        if (verbose) {
                            log.info(
                                'Creating directory "' +
                                dirPath +
                                '"');
                        }
                        return qfs.makeTree(dirPath);
                    }
                })
                .then(constant(dirPath)));
        },

        // Promise to write a file to disk.
        write = function(build, content, pattern) {
            return (
                q.all([build, content, pattern])
                .then(destructured(function(build, content, pattern) {
                    return (
                        qfs.exists(build)
                        .then(function(exists) {
                            if (!overwrite && exists) {
                                throw new Error(
                                    'The overwrite setting is false ' +
                                    'and the file "' +
                                    build +
                                    '" already exists');
                            }
                            return (
                                mkdirs(build)
                                .then(function() {
                                    if (verbose) {
                                        (exists ? log.warn : log.info)(
                                            'Rendering ' +
                                            (exists ?
                                                '(overwrite) ' :
                                                '') +
                                            '"' +
                                            build +
                                            '" for URL pattern "' +
                                            pattern.name +
                                            '"');
                                    }
                                    return qfs.write(build, content);
                                }));
                        })
                        .then(constant(build)));
                })));
        };

    //------------------
    //    Public API
    //------------------

    return function() {
        try {
            return (
                q.call.sequence.all(
                    middleware.get.pre.build(),
                    require('../formidable'))
                .then(function() {
                    return (
                        urls.include(option('urls') || 'urls')
                        .then(function(patterns) {
                            return (
                                q.map(patterns, function(pattern) {
                                    return (
                                        q.when(
                                            isString(pattern.view) ?
                                                (path.js(pattern.view)
                                                    .then(compose(q.when, require))) :
                                                pattern.view)
                                        .then(function(views) {
                                            return (
                                                q.map(isArray(views) ? views : [views], function(view) {
                                                    return (
                                                        q.all(view)
                                                        .then(function(view) {
                                                            var url = urls.resolve(pattern.name, view.params),
                                                                build = path.build(url);

                                                            if (!path.path.extname(build)) {
                                                                build = (
                                                                    path.path.join(
                                                                        build, 'index.html'));
                                                            }
                                                            return (
                                                                context.meta(
                                                                    view.context || context(),
                                                                    {
                                                                        url: url,
                                                                        params: view.params || {},
                                                                        template: view.template,
                                                                        build: build
                                                                    })
                                                                .then(function(context) {
                                                                    return (
                                                                        write(
                                                                            build,
                                                                            template.render(
                                                                                view.template,
                                                                                context),
                                                                            pattern));
                                                                }));
                                                        }));
                                                }));
                                        }));
                                }));
                        }));
                })
                .then(function() {
                    return (
                        q.call.sequence.all(
                            middleware.get.post.build(),
                            require('../formidable')));
                })
                .then(constant())
                .fail(raise));
        } catch (error) {
            raise(error);
        }
    };
};
<commit_msg>Views may now return a function for the template property that will be passed the context and expected to return a string or a promise for a string, bypassing the templating engine.<commit_after>'use strict';

module.exports = function(option, log, path, context, middleware, urls, template) {

    var //---------------
        //    Imports
        //---------------

        qfs = require('q-io/fs'),
        q = require('mercenary/promises'),
        lang = require('mercenary/lang'),
        constant = lang.constant,
        compose = lang.compose,
        destructured = lang.destructured,
        isArray = lang.is.array,
        isFunction = lang.is.fn,
        isString = lang.is.string,

        //---------------
        //    Options
        //---------------

        overwrite = option('overwrite') !== false,
        verbose = option('verbose') || false,

        //----------------------
        //    Implementation
        //----------------------

        // Raise an error using the fail log.
        raise = function(error) {
            log.fail(error.stack || 'An unexpected error occurred');
        },

        // Promise to fill in missing directories for the given build path.
        mkdirs = function(build) {
            var dirPath = path.path.dirname(build);

            return (
                qfs.exists(dirPath)
                .then(function(exists) {
                    if (exists) {
                        return (
                            qfs.stat(dirPath)
                            .then(function(stat) {
                                if (!stat.isDirectory()) {
                                    throw new Error(
                                        'The path "' +
                                        dirPath +
                                        '" is not a directory');
                                }
                            }));
                    } else {
                        if (verbose) {
                            log.info(
                                'Creating directory "' +
                                dirPath +
                                '"');
                        }
                        return qfs.makeTree(dirPath);
                    }
                })
                .then(constant(dirPath)));
        },

        // Promise to write a file to disk.
        write = function(build, content, pattern) {
            return (
                q.all([build, content, pattern])
                .then(destructured(function(build, content, pattern) {
                    return (
                        qfs.exists(build)
                        .then(function(exists) {
                            if (!overwrite && exists) {
                                throw new Error(
                                    'The overwrite setting is false ' +
                                    'and the file "' +
                                    build +
                                    '" already exists');
                            }
                            return (
                                mkdirs(build)
                                .then(function() {
                                    if (verbose) {
                                        (exists ? log.warn : log.info)(
                                            'Rendering ' +
                                            (exists ?
                                                '(overwrite) ' :
                                                '') +
                                            '"' +
                                            build +
                                            '" for URL pattern "' +
                                            pattern.name +
                                            '"');
                                    }
                                    return qfs.write(build, content);
                                }));
                        })
                        .then(constant(build)));
                })));
        };

    //------------------
    //    Public API
    //------------------

    return function() {
        try {
            return (
                q.call.sequence.all(
                    middleware.get.pre.build(),
                    require('../formidable'))
                .then(function() {
                    return (
                        urls.include(option('urls') || 'urls')
                        .then(function(patterns) {
                            return (
                                q.map(patterns, function(pattern) {
                                    return (
                                        q.when(
                                            isString(pattern.view) ?
                                                (path.js(pattern.view)
                                                    .then(compose(q.when, require))) :
                                                pattern.view)
                                        .then(function(views) {
                                            return (
                                                q.map(isArray(views) ? views : [views], function(view) {
                                                    return (
                                                        q.all(view)
                                                        .then(function(view) {
                                                            var url = urls.resolve(pattern.name, view.params),
                                                                build = path.build(url);

                                                            if (!path.path.extname(build)) {
                                                                build = (
                                                                    path.path.join(
                                                                        build, 'index.html'));
                                                            }
                                                            return (
                                                                context.meta(
                                                                    view.context || context(),
                                                                    {
                                                                        url: url,
                                                                        build: build
                                                                    })
                                                                .then(function(context) {
                                                                    return (
                                                                        write(
                                                                            build,
                                                                            isFunction(view.template) ?
                                                                                view.template(context) :
                                                                                template.render(
                                                                                view.template,
                                                                                context),
                                                                            pattern));
                                                                }));
                                                        }));
                                                }));
                                        }));
                                }));
                        }));
                })
                .then(function() {
                    return (
                        q.call.sequence.all(
                            middleware.get.post.build(),
                            require('../formidable')));
                })
                .then(constant())
                .fail(raise));
        } catch (error) {
            raise(error);
        }
    };
};
<|endoftext|>


<commit_before>/* eslint-disable */

var webpack = require('webpack');
var notifier = require('node-notifier');
var _ = require('lodash');
var fs = require('fs');

const nodeEnv = process.env.NODE_ENV || 'development';

module.exports = function(config) {
  config.builds.forEach(exectuteBuild);
}

function exectuteBuild(build) {
  var compiler = webpack(build.webpackConfig);
  var buildName = build.name;
  if (build.watch) {
    compiler.watch({}, outputBuild(buildName));
  } else {
    compiler.run(function(err, stats) {
      console.log(stats.toString({
        colors: !build.disableColors,
        chunks: false,
        version: false,
      }));
    });
  }
}

function formatAsset(asset) {
  var name = asset.name;
  var size = asset.size;
  var sizeStr = size + " B";

  if (size > 1000) sizeStr = Math.ceil(size / 1000) + " kB";
  if (size > 1000000) sizeStr = Math.ceil(size / 1000000) + " MB";

  return name + " [" + sizeStr + "]";
}

function outputBuild(type) {
  return function(err, stats) {
    if (!err) {
      console.log(stats.toString({
        colors: true,
        chunks: false,
        version: false,
      }));

      var s = stats.toJson();

      if (s.errors && s.errors.length) {
        notifier.notify({
          "title": type + " – ERROR!",
          "message": "Check the console for errors",
        });
      } else {
        notifier.notify({
          "title": type + " – Build complete",
          "message": s.assets.map(formatAsset).join("\n"),
        });
      }
    }
  }
}
<commit_msg>Fix buildname notifier output<commit_after>/* eslint-disable */

var webpack = require('webpack');
var notifier = require('node-notifier');
var _ = require('lodash');
var fs = require('fs');

const nodeEnv = process.env.NODE_ENV || 'development';

module.exports = function(config) {
  config.builds.forEach(exectuteBuild);
}

function exectuteBuild(build) {
  var compiler = webpack(build.webpackConfig);
  if (build.watch) {
    compiler.watch({}, outputBuild(build.buildName));
  } else {
    compiler.run(function(err, stats) {
      console.log(stats.toString({
        colors: !build.disableColors,
        chunks: false,
        version: false,
      }));
    });
  }
}

function formatAsset(asset) {
  var name = asset.name;
  var size = asset.size;
  var sizeStr = size + " B";

  if (size > 1000) sizeStr = Math.ceil(size / 1000) + " kB";
  if (size > 1000000) sizeStr = Math.ceil(size / 1000000) + " MB";

  return name + " [" + sizeStr + "]";
}

function outputBuild(type) {
  return function(err, stats) {
    if (!err) {
      console.log(stats.toString({
        colors: true,
        chunks: false,
        version: false,
      }));

      var s = stats.toJson();

      if (s.errors && s.errors.length) {
        notifier.notify({
          "title": type + " – ERROR!",
          "message": "Check the console for errors",
        });
      } else {
        notifier.notify({
          "title": type + " – Build complete",
          "message": s.assets.map(formatAsset).join("\n"),
        });
      }
    }
  }
}
<|endoftext|>


<commit_before>const path                 = require('path')
const cache                = require('memory-cache')
const cheerio              = require('cheerio')
const emailRemoveUnusedCss = require('email-remove-unused-css')
const he                   = require('he')
const juice                = require('juice')
const { minify }           = require('html-minifier')
const Promise              = require('bluebird')

const logger    = require('./logger.js')
const ImageSize = require('./image-size')
const utils     = require('./utils.js')

const Build = function(tplPath) {
    this.tplPath = tplPath
    this.tplName = path.parse(tplPath).base
    this.config = cache.get('config')
    this.imageSize = new ImageSize(tplPath)
}

Build.prototype.go = function() {
    return this.get()
        .then(this.saveEmails.bind(this))
        .catch(err => {
            logger.error(err)
        })
}

Build.prototype.get = function() {

    const handlebars = utils.requireAndInit('handlebars', this.tplPath)
    const styles     = utils.requireAndInit('styles', this.tplPath)

    const promises = [
        handlebars.get(),
        styles.get()
    ]

    return Promise.all(promises)
        .then(([ { html, text }, css ]) => {

            // text version
            text = this.cleanTextSpecialChars(text)
            this.text = text

            // enforce doctype
            html = this.setDoctype(html)

            // process HTML
            return Promise.all([
                this.generateProdHtml(html, css),
                this.generateDevHtml(html, css)
            ])

        })
        .then(([htmlProd, htmlDev]) => {

            this.html = htmlProd
            this.htmlDev = htmlDev

            return {
                html: this.html,
                htmlDev: this.htmlDev,
                text: this.text
            }

        })
        .catch(err => {
            logger.error(err)
        })

}

Build.prototype.generateProdHtml = function(html, css) {

    const $ = cheerio.load(html)
    this.defaultAttrs($)
    this.emptyCells($)
    this.injectHeadStyles($, css.head)
    this.injectInlineStyles($, css.inline)

    return this.imageSize.setAll($, true).then($ => {
        let html = $.html()
        html = this.removeUnusedCss(html)

        // TEMPORARY: delete once this issue is resolved:
        // https://github.com/code-and-send/detergent/issues/11
        html = html.replace(''', '’')

        return minify(html, {
            removeComments: true,
            removeCommentsFromCDATA: true,
            removeCDATASectionsFromCDATA: true,
            collapseWhitespace: true,
            minifyCSS: true
        })
    })

}

Build.prototype.generateDevHtml = function(html, css) {

    const $ = cheerio.load(html)

    const getStyleFilename = style => {
        return this.config.files[style].replace('.scss', '.css')
    }

    const getStylesheet = styleName => {

        let filePath

        // if this is the main stylesheet or
        // if this is a custom reset stylesheet
        if (styleName === 'styles' || css.customResets[styleName]) {
            filePath = `css/${getStyleFilename(styleName)}`
        }

        // otherwise, this must be a common stylesheet
        else {
            filePath = `../common/${getStyleFilename(styleName)}`
        }

        // build a DOM node for this stylesheet
        return $('<link/>').attr({
            rel: 'stylesheet',
            type: 'text/css',
            href: filePath
        })

    }

    const headStylesheet   = getStylesheet('resetHead')
    const inlineStylesheet = getStylesheet('resetInline')
    const stylesStylesheet = getStylesheet('styles')

    this.defaultAttrs($)
    this.emptyCells($)

    // TODO: move to separate function
    $('head')
        .append('\n<!-- BEGIN DEVELOPMENT VERSION STYLESHEET INJECTS -->\n')
        .append(headStylesheet)
        .append(inlineStylesheet)
        .append(stylesStylesheet)
        .append('\n<!-- END DEVELOPMENT VERSION STYLESHEET INJECTS -->\n\n')

    return this.imageSize.setAll($).then($ => $.html())

}

Build.prototype.injectHeadStyles = function($, styles) {

    if (!$('head style').length) {
        $('head').append('<style type="text/css"></style>')
    }

    $('head style').append(styles)

    return $

}

Build.prototype.setDoctype = function(html) {
    // enforce doctype specified in the config
    // strip the existing doctype if it exists
    html = html.trim().replace(/^<!doctype(.*?)>(\s*)/i, '')
    return `${this.config.doctype}\n${html}`
}

Build.prototype.injectInlineStyles = function($, styles) {
    // inline styles using Juice and clean up extra whitespace
    const opts = {
        applyAttributesTableElements: true,
        applyHeightAttributes: true,
        applyWidthAttributes: true
    }
    juice.inlineDocument($, styles, opts)
    return $
}

Build.prototype.defaultAttrs = function($) {

    $('table').attr({
        cellpadding: 0,
        cellspacing: 0,
        border: 0
    })

    $('img').attr({
        border: 0
    })

    $('a').attr({
        target: '_blank'
    })

    return $
}

Build.prototype.emptyCells = function($) {

    $('td').each(function() {
        if (!$(this).html().trim()) {
            $(this).html(' ')
        }
    })

    return $

}

Build.prototype.cleanTextSpecialChars = function(text) {
    // replace non-ASCII characters with ASCII equivalents
    // removes any remaining non-ASCII characters
    return he.decode(text)
        .replace(/[\u2018\u2019]/g, '\'')
        .replace(/[\u201C\u201D]/g, '"')
        .replace(/[\u2013\u2014]/g, '-')
        .replace(/[\u2026]/g, '...')
        .replace(/[^\x00-\x7F]/g, '') // eslint-disable-line no-control-regex
}

Build.prototype.removeUnusedCss = function(html) {
    return emailRemoveUnusedCss(html, {
        whitelist: ['#outlook', '.ExternalClass', '.ReadMsgBody']
    })[0]
}

Build.prototype.saveEmails = function() {

    const promises = [
        utils.saveEmail(this.html, this.tplName, 'html'),
        utils.saveEmail(this.htmlDev, this.tplName, 'dev.html'),
        utils.saveEmail(this.text, this.tplName, 'txt')
    ]

    return Promise.all(promises).catch(err => {
        logger.error(err)
    })

}

module.exports = Build
<commit_msg>add #preview-text to whitelist<commit_after>const path                 = require('path')
const cache                = require('memory-cache')
const cheerio              = require('cheerio')
const emailRemoveUnusedCss = require('email-remove-unused-css')
const he                   = require('he')
const juice                = require('juice')
const { minify }           = require('html-minifier')
const Promise              = require('bluebird')

const logger    = require('./logger.js')
const ImageSize = require('./image-size')
const utils     = require('./utils.js')

const Build = function(tplPath) {
    this.tplPath = tplPath
    this.tplName = path.parse(tplPath).base
    this.config = cache.get('config')
    this.imageSize = new ImageSize(tplPath)
}

Build.prototype.go = function() {
    return this.get()
        .then(this.saveEmails.bind(this))
        .catch(err => {
            logger.error(err)
        })
}

Build.prototype.get = function() {

    const handlebars = utils.requireAndInit('handlebars', this.tplPath)
    const styles     = utils.requireAndInit('styles', this.tplPath)

    const promises = [
        handlebars.get(),
        styles.get()
    ]

    return Promise.all(promises)
        .then(([ { html, text }, css ]) => {

            // text version
            text = this.cleanTextSpecialChars(text)
            this.text = text

            // enforce doctype
            html = this.setDoctype(html)

            // process HTML
            return Promise.all([
                this.generateProdHtml(html, css),
                this.generateDevHtml(html, css)
            ])

        })
        .then(([htmlProd, htmlDev]) => {

            this.html = htmlProd
            this.htmlDev = htmlDev

            return {
                html: this.html,
                htmlDev: this.htmlDev,
                text: this.text
            }

        })
        .catch(err => {
            logger.error(err)
        })

}

Build.prototype.generateProdHtml = function(html, css) {

    const $ = cheerio.load(html)
    this.defaultAttrs($)
    this.emptyCells($)
    this.injectHeadStyles($, css.head)
    this.injectInlineStyles($, css.inline)

    return this.imageSize.setAll($, true).then($ => {
        let html = $.html()
        html = this.removeUnusedCss(html)

        // TEMPORARY: delete once this issue is resolved:
        // https://github.com/code-and-send/detergent/issues/11
        html = html.replace(''', '’')

        return minify(html, {
            removeComments: true,
            removeCommentsFromCDATA: true,
            removeCDATASectionsFromCDATA: true,
            collapseWhitespace: true,
            minifyCSS: true
        })
    })

}

Build.prototype.generateDevHtml = function(html, css) {

    const $ = cheerio.load(html)

    const getStyleFilename = style => {
        return this.config.files[style].replace('.scss', '.css')
    }

    const getStylesheet = styleName => {

        let filePath

        // if this is the main stylesheet or
        // if this is a custom reset stylesheet
        if (styleName === 'styles' || css.customResets[styleName]) {
            filePath = `css/${getStyleFilename(styleName)}`
        }

        // otherwise, this must be a common stylesheet
        else {
            filePath = `../common/${getStyleFilename(styleName)}`
        }

        // build a DOM node for this stylesheet
        return $('<link/>').attr({
            rel: 'stylesheet',
            type: 'text/css',
            href: filePath
        })

    }

    const headStylesheet   = getStylesheet('resetHead')
    const inlineStylesheet = getStylesheet('resetInline')
    const stylesStylesheet = getStylesheet('styles')

    this.defaultAttrs($)
    this.emptyCells($)

    // TODO: move to separate function
    $('head')
        .append('\n<!-- BEGIN DEVELOPMENT VERSION STYLESHEET INJECTS -->\n')
        .append(headStylesheet)
        .append(inlineStylesheet)
        .append(stylesStylesheet)
        .append('\n<!-- END DEVELOPMENT VERSION STYLESHEET INJECTS -->\n\n')

    return this.imageSize.setAll($).then($ => $.html())

}

Build.prototype.injectHeadStyles = function($, styles) {

    if (!$('head style').length) {
        $('head').append('<style type="text/css"></style>')
    }

    $('head style').append(styles)

    return $

}

Build.prototype.setDoctype = function(html) {
    // enforce doctype specified in the config
    // strip the existing doctype if it exists
    html = html.trim().replace(/^<!doctype(.*?)>(\s*)/i, '')
    return `${this.config.doctype}\n${html}`
}

Build.prototype.injectInlineStyles = function($, styles) {
    // inline styles using Juice and clean up extra whitespace
    const opts = {
        applyAttributesTableElements: true,
        applyHeightAttributes: true,
        applyWidthAttributes: true
    }
    juice.inlineDocument($, styles, opts)
    return $
}

Build.prototype.defaultAttrs = function($) {

    $('table').attr({
        cellpadding: 0,
        cellspacing: 0,
        border: 0
    })

    $('img').attr({
        border: 0
    })

    $('a').attr({
        target: '_blank'
    })

    return $
}

Build.prototype.emptyCells = function($) {

    $('td').each(function() {
        if (!$(this).html().trim()) {
            $(this).html(' ')
        }
    })

    return $

}

Build.prototype.cleanTextSpecialChars = function(text) {
    // replace non-ASCII characters with ASCII equivalents
    // removes any remaining non-ASCII characters
    return he.decode(text)
        .replace(/[\u2018\u2019]/g, '\'')
        .replace(/[\u201C\u201D]/g, '"')
        .replace(/[\u2013\u2014]/g, '-')
        .replace(/[\u2026]/g, '...')
        .replace(/[^\x00-\x7F]/g, '') // eslint-disable-line no-control-regex
}

Build.prototype.removeUnusedCss = function(html) {
    return emailRemoveUnusedCss(html, {
        whitelist: ['#outlook', '.ExternalClass', '.ReadMsgBody', '#preview-text']
    })[0]
}

Build.prototype.saveEmails = function() {

    const promises = [
        utils.saveEmail(this.html, this.tplName, 'html'),
        utils.saveEmail(this.htmlDev, this.tplName, 'dev.html'),
        utils.saveEmail(this.text, this.tplName, 'txt')
    ]

    return Promise.all(promises).catch(err => {
        logger.error(err)
    })

}

module.exports = Build
<|endoftext|>


<commit_before>
module.exports = exports = build

/**
 * Module dependencies.
 */

var fs = require('graceful-fs')
  , rm = require('rimraf')
  , path = require('path')
  , glob = require('glob')
  , log = require('npmlog')
  , which = require('which')
  , mkdirp = require('mkdirp')
  , win = process.platform == 'win32'

exports.usage = 'Invokes `' + (win ? 'msbuild' : 'make') + '` and builds the module'

function build (gyp, argv, callback) {

  var makeCommand = gyp.opts.make || process.env.MAKE
    || (process.platform.indexOf('bsd') != -1 ? 'gmake' : 'make')
  var command = win ? 'msbuild' : makeCommand
    , buildDir = path.resolve('build')
    , configPath = path.resolve(buildDir, 'config.gypi')
    , buildType
    , config
    , arch
    , nodeDir
    , copyDevLib

  loadConfigGypi()

  /**
   * Load the "config.gypi" file that was generated during "configure".
   */

  function loadConfigGypi () {
    fs.readFile(configPath, 'utf8', function (err, data) {
      if (err) {
        if (err.code == 'ENOENT') {
          callback(new Error('You must run `node-gyp configure` first!'))
        } else {
          callback(err)
        }
        return
      }
      config = JSON.parse(data.replace(/\#.+\n/, ''))

      // get the 'arch', 'buildType', and 'nodeDir' vars from the config
      buildType = config.target_defaults.default_configuration
      arch = config.variables.target_arch
      nodeDir = config.variables.nodedir
      copyDevLib = config.variables.copy_dev_lib == 'true'

      if ('debug' in gyp.opts) {
        buildType = gyp.opts.debug ? 'Debug' : 'Release'
      }
      if (!buildType) {
        buildType = 'Release'
      }

      log.verbose('build type:', buildType)
      log.verbose('architecture:', arch)
      log.verbose('node dev dir:', nodeDir)

      if (win) {
        findSolutionFile()
      } else {
        doWhich()
      }
    })
  }

  /**
   * On Windows, find the first build/*.sln file.
   */

  function findSolutionFile () {
    glob('build/*.sln', function (err, files) {
      if (err) return callback(err)
      if (files.length === 0) {
        return callback(new Error('Could not find *.sln file. Did you run "configure"?'))
      }
      guessedSolution = files[0]
      log.verbose('found first Solution file', guessedSolution)
      doWhich()
    })
  }

  /**
   * Uses node-which to locate the msbuild / make executable.
   */

  function doWhich () {
    // First make sure we have the build command in the PATH
    which(command, function (err, execPath) {
      if (err) {
        if (win && /not found/.test(err.message)) {
          // On windows and no 'msbuild' found. Let's guess where it is
          guessMsbuild()
        } else {
          // Some other error or 'make' not found on Unix, report that to the user
          callback(err)
        }
        return
      }
      log.verbose('`which` succeeded for `' + command + '`', execPath)
      copyNodeLib()
    })
  }

  /**
   * Guess the location of the "msbuild.exe" file on Windows.
   */

  function guessMsbuild () {
    log.verbose('could not find "msbuild.exe". guessing location')
    // This is basically just hard-coded. If this causes problems
    // then we'll think of something more clever.
    var windir = process.env.WINDIR || process.env.SYSTEMROOT || 'C:\\WINDOWS'
      , frameworkDir = path.resolve(windir, 'Microsoft.NET', 'Framework')
      , versionDir = path.resolve(frameworkDir, 'v4.0.30319') // This is probably the most brittle part...
      , msbuild = path.resolve(versionDir, 'msbuild.exe')
    fs.stat(msbuild, function (err, stat) {
      if (err) {
        if (err.code == 'ENOENT') {
          callback(new Error('Can\'t find "msbuild.exe". Do you have Microsoft Visual Studio C++ 2010 installed?'))
        } else {
          callback(err)
        }
        return
      }
      command = msbuild
      copyNodeLib()
    })
  }

  /**
   * Copies the node.lib file for the current target architecture into the
   * current proper dev dir location.
   */

  function copyNodeLib () {
    if (!win || !copyDevLib) return doBuild()

    var buildDir = path.resolve(nodeDir, buildType)
      , archNodeLibPath = path.resolve(nodeDir, arch, 'node.lib')
      , buildNodeLibPath = path.resolve(buildDir, 'node.lib')

    mkdirp(buildDir, function (err, isNew) {
      if (err) return callback(err)
      log.verbose('"' + buildType + '" dir needed to be created?', isNew)
      var rs = fs.createReadStream(archNodeLibPath)
        , ws = fs.createWriteStream(buildNodeLibPath)
      log.verbose('copying "node.lib" for ' + arch, buildNodeLibPath)
      rs.pipe(ws)
      rs.on('error', callback)
      ws.on('error', callback)
      rs.on('end', doBuild)
    })
  }

  /**
   * Actually spawn the process and compile the module.
   */

  function doBuild () {

    // Enable Verbose build
    var verbose = log.levels[log.level] <= log.levels.verbose
    if (!win && verbose) {
      argv.push('V=1')
    }
    if (win && !verbose) {
      argv.push('/clp:Verbosity=minimal')
    }

    // Turn off the Microsoft logo on Windows
    if (win) {
      argv.push('/nologo')
    }

    // Specify the build type, Release by default
    if (win) {
      var p = arch === 'x64' ? 'x64' : 'Win32'
      argv.push('/p:Configuration=' + buildType + ';Platform=' + p)
    } else {
      argv.push('BUILDTYPE=' + buildType)
      // Invoke the Makefile in the 'build' dir.
      argv.push('-C')
      argv.push('build')
    }

    if (win) {
      // did the user specify their own .sln file?
      var hasSln = argv.some(function (arg) {
        return path.extname(arg) == '.sln'
      })
      if (!hasSln) {
        argv.unshift(gyp.opts.solution || guessedSolution)
      }
    }

    var proc = gyp.spawn(command, argv)
    proc.on('exit', onExit)
  }

  /**
   * Invoked after the make/msbuild command exits.
   */

  function onExit (code, signal) {
    if (code !== 0) {
      return callback(new Error('`' + command + '` failed with exit code: ' + code))
    }
    if (signal) {
      return callback(new Error('`' + command + '` got signal: ' + signal))
    }
    //symlinkNodeBinding()
    callback()
  }

  function symlinkNodeBinding () {
    var buildDir = path.join('build', buildType, '*.node')
    log.verbose('globbing for files', buildDir)
    glob(buildDir, function (err, nodeFiles) {
      if (err) return callback(err)
      function link () {
        var file = nodeFiles.shift()
        if (!file) {
          // no more files to link... done!
          return callback()
        }
        var dest = path.join('build', path.basename(file))
        log.info('symlink', 'creating link %j pointing to %j', file, dest)
        var rel = path.relative('build', file)
        log.verbose('symlink data', rel)
        fs.symlink(rel, dest, 'file', function (err) {
          if (err) {
            if (err.code === 'EEXIST') {
              log.verbose('destination already exists; deleting', dest)
              rm(dest, function (err) {
                if (err) return callback(err)
                log.verbose('delete successful; trying symlink again')
                nodeFiles.unshift(file)
                link()
              })
            } else {
              callback(err)
            }
            return
          }
          // process the next file, if any
          link()
        })
      }
      // start linking
      link()
    })
  }

}
<commit_msg>build: fix logging<commit_after>
module.exports = exports = build

/**
 * Module dependencies.
 */

var fs = require('graceful-fs')
  , rm = require('rimraf')
  , path = require('path')
  , glob = require('glob')
  , log = require('npmlog')
  , which = require('which')
  , mkdirp = require('mkdirp')
  , win = process.platform == 'win32'

exports.usage = 'Invokes `' + (win ? 'msbuild' : 'make') + '` and builds the module'

function build (gyp, argv, callback) {

  var makeCommand = gyp.opts.make || process.env.MAKE
    || (process.platform.indexOf('bsd') != -1 ? 'gmake' : 'make')
  var command = win ? 'msbuild' : makeCommand
    , buildDir = path.resolve('build')
    , configPath = path.resolve(buildDir, 'config.gypi')
    , buildType
    , config
    , arch
    , nodeDir
    , copyDevLib

  loadConfigGypi()

  /**
   * Load the "config.gypi" file that was generated during "configure".
   */

  function loadConfigGypi () {
    fs.readFile(configPath, 'utf8', function (err, data) {
      if (err) {
        if (err.code == 'ENOENT') {
          callback(new Error('You must run `node-gyp configure` first!'))
        } else {
          callback(err)
        }
        return
      }
      config = JSON.parse(data.replace(/\#.+\n/, ''))

      // get the 'arch', 'buildType', and 'nodeDir' vars from the config
      buildType = config.target_defaults.default_configuration
      arch = config.variables.target_arch
      nodeDir = config.variables.nodedir
      copyDevLib = config.variables.copy_dev_lib == 'true'

      if ('debug' in gyp.opts) {
        buildType = gyp.opts.debug ? 'Debug' : 'Release'
      }
      if (!buildType) {
        buildType = 'Release'
      }

      log.verbose('build type', buildType)
      log.verbose('architecture', arch)
      log.verbose('node dev dir', nodeDir)

      if (win) {
        findSolutionFile()
      } else {
        doWhich()
      }
    })
  }

  /**
   * On Windows, find the first build/*.sln file.
   */

  function findSolutionFile () {
    glob('build/*.sln', function (err, files) {
      if (err) return callback(err)
      if (files.length === 0) {
        return callback(new Error('Could not find *.sln file. Did you run "configure"?'))
      }
      guessedSolution = files[0]
      log.verbose('found first Solution file', guessedSolution)
      doWhich()
    })
  }

  /**
   * Uses node-which to locate the msbuild / make executable.
   */

  function doWhich () {
    // First make sure we have the build command in the PATH
    which(command, function (err, execPath) {
      if (err) {
        if (win && /not found/.test(err.message)) {
          // On windows and no 'msbuild' found. Let's guess where it is
          guessMsbuild()
        } else {
          // Some other error or 'make' not found on Unix, report that to the user
          callback(err)
        }
        return
      }
      log.verbose('`which` succeeded for `' + command + '`', execPath)
      copyNodeLib()
    })
  }

  /**
   * Guess the location of the "msbuild.exe" file on Windows.
   */

  function guessMsbuild () {
    log.verbose('could not find "msbuild.exe". guessing location')
    // This is basically just hard-coded. If this causes problems
    // then we'll think of something more clever.
    var windir = process.env.WINDIR || process.env.SYSTEMROOT || 'C:\\WINDOWS'
      , frameworkDir = path.resolve(windir, 'Microsoft.NET', 'Framework')
      , versionDir = path.resolve(frameworkDir, 'v4.0.30319') // This is probably the most brittle part...
      , msbuild = path.resolve(versionDir, 'msbuild.exe')
    fs.stat(msbuild, function (err, stat) {
      if (err) {
        if (err.code == 'ENOENT') {
          callback(new Error('Can\'t find "msbuild.exe". Do you have Microsoft Visual Studio C++ 2010 installed?'))
        } else {
          callback(err)
        }
        return
      }
      command = msbuild
      copyNodeLib()
    })
  }

  /**
   * Copies the node.lib file for the current target architecture into the
   * current proper dev dir location.
   */

  function copyNodeLib () {
    if (!win || !copyDevLib) return doBuild()

    var buildDir = path.resolve(nodeDir, buildType)
      , archNodeLibPath = path.resolve(nodeDir, arch, 'node.lib')
      , buildNodeLibPath = path.resolve(buildDir, 'node.lib')

    mkdirp(buildDir, function (err, isNew) {
      if (err) return callback(err)
      log.verbose('"' + buildType + '" dir needed to be created?', isNew)
      var rs = fs.createReadStream(archNodeLibPath)
        , ws = fs.createWriteStream(buildNodeLibPath)
      log.verbose('copying "node.lib" for ' + arch, buildNodeLibPath)
      rs.pipe(ws)
      rs.on('error', callback)
      ws.on('error', callback)
      rs.on('end', doBuild)
    })
  }

  /**
   * Actually spawn the process and compile the module.
   */

  function doBuild () {

    // Enable Verbose build
    var verbose = log.levels[log.level] <= log.levels.verbose
    if (!win && verbose) {
      argv.push('V=1')
    }
    if (win && !verbose) {
      argv.push('/clp:Verbosity=minimal')
    }

    // Turn off the Microsoft logo on Windows
    if (win) {
      argv.push('/nologo')
    }

    // Specify the build type, Release by default
    if (win) {
      var p = arch === 'x64' ? 'x64' : 'Win32'
      argv.push('/p:Configuration=' + buildType + ';Platform=' + p)
    } else {
      argv.push('BUILDTYPE=' + buildType)
      // Invoke the Makefile in the 'build' dir.
      argv.push('-C')
      argv.push('build')
    }

    if (win) {
      // did the user specify their own .sln file?
      var hasSln = argv.some(function (arg) {
        return path.extname(arg) == '.sln'
      })
      if (!hasSln) {
        argv.unshift(gyp.opts.solution || guessedSolution)
      }
    }

    var proc = gyp.spawn(command, argv)
    proc.on('exit', onExit)
  }

  /**
   * Invoked after the make/msbuild command exits.
   */

  function onExit (code, signal) {
    if (code !== 0) {
      return callback(new Error('`' + command + '` failed with exit code: ' + code))
    }
    if (signal) {
      return callback(new Error('`' + command + '` got signal: ' + signal))
    }
    //symlinkNodeBinding()
    callback()
  }

  function symlinkNodeBinding () {
    var buildDir = path.join('build', buildType, '*.node')
    log.verbose('globbing for files', buildDir)
    glob(buildDir, function (err, nodeFiles) {
      if (err) return callback(err)
      function link () {
        var file = nodeFiles.shift()
        if (!file) {
          // no more files to link... done!
          return callback()
        }
        var dest = path.join('build', path.basename(file))
        log.info('symlink', 'creating link %j pointing to %j', file, dest)
        var rel = path.relative('build', file)
        log.verbose('symlink data', rel)
        fs.symlink(rel, dest, 'file', function (err) {
          if (err) {
            if (err.code === 'EEXIST') {
              log.verbose('destination already exists; deleting', dest)
              rm(dest, function (err) {
                if (err) return callback(err)
                log.verbose('delete successful; trying symlink again')
                nodeFiles.unshift(file)
                link()
              })
            } else {
              callback(err)
            }
            return
          }
          // process the next file, if any
          link()
        })
      }
      // start linking
      link()
    })
  }

}
<|endoftext|>


<commit_before>var async    = require('async');
var cron     = require('cron');
var fetcher  = require('./fetcher');
var levelup  = require('level');
var levelttl = require('level-ttl');

const MAX_FETCH        = 5;
const REFRESH_SCHEDULE = '0 0 */1 * * *';

function Cache(categoryList, urlLookup, opts) {
  this.categoryList = categoryList;
  this.urlLookup    = urlLookup;

  this.db = levelttl(levelup(opts.dbDir), {
    defaultTTL: opts.defaultTtl,
    checkFrequency: opts.checkFrequency
  });
}

Cache.prototype.build = function (cb) {
  var self = this;
  console.log('[i] Building cache at ' + new Date());

  var feedTasks = [];
  this.categoryList.forEach(function (category) {
    category.feeds.forEach(function (feed) {
      function feedTask(cb) {
        self.setFeed(feed.url, cb);
      }
      feedTasks.push(feedTask);
    });
  });
  async.parallelLimit(feedTasks, MAX_FETCH, function (err, articlesLists) {
    if (err) {
      cb(err);
    } else {

      var articleTasks = [];
      articlesLists.forEach(function (articlesList) {

        if (Array.isArray(articlesList)) {
          articlesList.forEach(function (article) {
            function articleTask(cb) {
              self.setArticle(article.url, cb);
            }
            articleTasks.push(articleTask);
          });
        } else {
          console.error('[x] Unexpected articlesList: ' + articlesList);
        }

      });
      async.parallelLimit(articleTasks, MAX_FETCH, function (err, results) {
        if (!err) {
          console.log('[v] Cache build done');
        }
        cb(err, results);
      });
    }
  });
};

Cache.prototype.refresh = function () {
  var self = this;
  console.log('[i] Scheduling cache refresh with ' + REFRESH_SCHEDULE);

  var CronJob = cron.CronJob;
  var job     = new CronJob(
    REFRESH_SCHEDULE,
    function () {
      console.log('[i] Refreshing cache at ' + new Date());
      self.build(function (err) {
        if (err) {
          console.error(err);
        }
      });
    },
    function () {
      console.log('[v] Cache refresh done');
    },
    true
  );
};

Cache.prototype.setFeed = function (feedUrl, cb) {
  var self = this;
  fetcher.fetchFeed(feedUrl, function (err, articlesList) {
console.dir(arguments)
    if (err) {
      cb();
    } else {
      self.db.put(feedUrl, JSON.stringify(articlesList), function (err) {
        cb(err, articlesList);
      });
    }
  });
};

Cache.prototype.setArticle = function (articleUrl, cb) {
  var self = this;
  fetcher.fetchArticle(articleUrl, function (article) {
    if (article.url === undefined) {
      console.error(new Error('[x] Unable to fetch article ' + articleUrl));
      cb();
    } else {
      // strip fragment identifier from article URL
      // fragment identifier is only available on the client browser and not on the server
      // some feeds, e.g. BBC's contain links with fragment identifier
      self.db.put(articleUrl.replace(/#.*$/, ''), JSON.stringify(article), cb);
    }
  });
};

Cache.prototype.getFeed = function (feedId, cb) {
  var url = this.urlLookup[feedId];
  this.db.get(url, function (err, result) {
    if (err) {
      cb(err);
    } else {
      cb(null, JSON.parse(result));
    }
  });
};

Cache.prototype.getArticle = function (url, cb) {
  this.db.get(url, function (err, result) {
    if (err) {
      cb(err);
    } else {
      cb(null, JSON.parse(result));
    }
  });
};

module.exports = Cache;<commit_msg>Clean up lint violation.<commit_after>var async    = require('async');
var cron     = require('cron');
var fetcher  = require('./fetcher');
var levelup  = require('level');
var levelttl = require('level-ttl');

const MAX_FETCH        = 5;
const REFRESH_SCHEDULE = '0 0 */1 * * *';

function Cache(categoryList, urlLookup, opts) {
  this.categoryList = categoryList;
  this.urlLookup    = urlLookup;

  this.db = levelttl(levelup(opts.dbDir), {
    defaultTTL: opts.defaultTtl,
    checkFrequency: opts.checkFrequency
  });
}

Cache.prototype.build = function (cb) {
  var self = this;
  console.log('[i] Building cache at ' + new Date());

  var feedTasks = [];
  this.categoryList.forEach(function (category) {
    category.feeds.forEach(function (feed) {
      function feedTask(cb) {
        self.setFeed(feed.url, cb);
      }
      feedTasks.push(feedTask);
    });
  });
  async.parallelLimit(feedTasks, MAX_FETCH, function (err, articlesLists) {
    if (err) {
      cb(err);
    } else {

      var articleTasks = [];
      articlesLists.forEach(function (articlesList) {

        if (Array.isArray(articlesList)) {
          articlesList.forEach(function (article) {
            function articleTask(cb) {
              self.setArticle(article.url, cb);
            }
            articleTasks.push(articleTask);
          });
        } else {
          console.error('[x] Unexpected articlesList: ' + articlesList);
        }

      });
      async.parallelLimit(articleTasks, MAX_FETCH, function (err, results) {
        if (!err) {
          console.log('[v] Cache build done');
        }
        cb(err, results);
      });
    }
  });
};

Cache.prototype.refresh = function () {
  var self = this;
  console.log('[i] Scheduling cache refresh with ' + REFRESH_SCHEDULE);

  var CronJob = cron.CronJob;
  var job     = new CronJob(
    REFRESH_SCHEDULE,
    function () {
      console.log('[i] Refreshing cache at ' + new Date());
      self.build(function (err) {
        if (err) {
          console.error(err);
        }
      });
    },
    function () {
      console.log('[v] Cache refresh done');
    },
    true
  );
};

Cache.prototype.setFeed = function (feedUrl, cb) {
  var self = this;
  fetcher.fetchFeed(feedUrl, function (err, articlesList) {
    if (err) {
      cb();
    } else {
      self.db.put(feedUrl, JSON.stringify(articlesList), function (err) {
        cb(err, articlesList);
      });
    }
  });
};

Cache.prototype.setArticle = function (articleUrl, cb) {
  var self = this;
  fetcher.fetchArticle(articleUrl, function (article) {
    if (article.url === undefined) {
      console.error(new Error('[x] Unable to fetch article ' + articleUrl));
      cb();
    } else {
      // strip fragment identifier from article URL
      // fragment identifier is only available on the client browser and not on the server
      // some feeds, e.g. BBC's contain links with fragment identifier
      self.db.put(articleUrl.replace(/#.*$/, ''), JSON.stringify(article), cb);
    }
  });
};

Cache.prototype.getFeed = function (feedId, cb) {
  var url = this.urlLookup[feedId];
  this.db.get(url, function (err, result) {
    if (err) {
      cb(err);
    } else {
      cb(null, JSON.parse(result));
    }
  });
};

Cache.prototype.getArticle = function (url, cb) {
  this.db.get(url, function (err, result) {
    if (err) {
      cb(err);
    } else {
      cb(null, JSON.parse(result));
    }
  });
};

module.exports = Cache;<|endoftext|>


<commit_before>'use strict';

var access = require('object-access');

var cache = {};

exports.set = function (filename, mtime, info) {
  access.set(cache, [filename, mtime], info);
};

exports.get = function (filename, mtime) {
  return access(cache, [filename, mtime]);
};

exports.remove = function (filename) {
  access.remove(cache, [filename]);
};

<commit_msg>enhances file cache<commit_after>'use strict';

'use strict';

module.exports = cache;
cache.cache = cached;
cache.Cache = Cache;

var access = require('object-access');
var AC = require('async-cache');
var fs = require('graceful-fs');

var cached = {};

function cache (file) {
  file = file || undefined;
  return cached[file] || (
    cached[file] = new Cache(file);
  );
}

function Cache (file) {
  if (!file) {
    // There is no file cache
    this.cache = {};
    return;
  }

  this.cache_file = file;
  var self = this;
  this.ac = new AC({
    max: 1000,
    maxAge: 1000 * 60 * 10,

    // actually, the key is fake
    load: function (key, callback) {
      fs.readFile(self.cache_file, function (err, content) {
        if (err) {
          return callback(err);
        }

        try {
          var json = JSON.parse(content.toString());
        } catch(e) {
          return callback(e);
        }

        self.cache = json;
      });
    }
  });

  process.on('exit', function () {
    fs.writeFileSync(self.cache_file, JSON.stringify(self.cache));
  });
}

Cache.prototype.ready = function(callback) {
  if (this.cache_file) {
    return this._ready(callback);
  }

  // if no cache file, emit ready directly
  callback(null);
};

Cache.prototype._ready = function(callback) {
  this.ac.get('', function (err) {
    callback(err);
  });
};

Cache.prototype.set = function (filename, mtime, info) {
  access.set(this.cache, [filename, mtime], info);
};

Cache.prototype.get = function (filename, mtime) {
  return access(this.cache, [filename, mtime]);
};

Cache.prototype.remove = function (filename) {
  access.remove(this.cache, [filename]);
};

<|endoftext|>


<commit_before>"use strict";

/**
 * Caching module using sqlite
 */

const sqlite3 = require("sqlite3");
const Moment = require("moment-timezone");
const Settings = require("./settings");
const DebugLog = require("./debugPrint.js");

let sqldb = null;
let pendingSetup = false;
let readyCallbacks = [];

// current database version, used for any future database scheme changes
const currentDBVersion = 1;

// get (or initialise) the cache database
function GetDB() {
    return new Promise((resolve) => {
        if (sqldb != null) {
            return resolve(sqldb);
        }

        // no setup yet, add our resolve to list of callbacks once db is ready
        readyCallbacks.push(resolve);

        // if we're already setting up elsewhere, return now and wait for our callback
        if (pendingSetup) return;
        pendingSetup = true;

        // no db setup yet, set one up
        // TODO - use filesystem
        const newdb = new sqlite3.Database(Settings.Cache, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, (err) => {
            if (err) {
                throw new Error(`Unable to initialise sqlite database: ${err}`);
            }

            if (process.env.DEBUG_DB) {
                newdb.on("trace", (query) => {
                    DebugLog("SQLite:", query);
                });
            }

            // create cache table
            SetupDB(newdb).then(() => {
                // upgrade database if needed
                return UpgradeDB(newdb);
            }).then(() => {
                // setup successful! store database reference for later and callback
                sqldb = newdb;

                // callback to all systems waiting for the database to be ready
                for (let i = 0, cb; cb = readyCallbacks[i++];) {
                    cb(sqldb);
                }
                readyCallbacks = [];
            }).catch(err => {
                throw new Error(`Failed to setup sqlite database: ${err}`);
            });
        });

    });
}

/**
 * Setup database tables
 */
function SetupDB(db) {
    return new Promise((resolve, reject) => {
        const setupQueries = [
            "CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)", // meta table, holds version data etc.
            "CREATE TABLE IF NOT EXISTS cache (key TEXT PRIMARY KEY, value TEXT, expires INTEGER NOT NULL)", // general cache table
            "CREATE TABLE IF NOT EXISTS disneyRides (id INT PRIMARY KEY, waitMinutes INTEGER, status INTEGER, document TEXT)", // couchbase-lite Disney documents
        ];

        const step = () => {
            const query = setupQueries.shift();

            if (!query) return resolve();

            db.run(query, (err) => {
                if (err) return reject(err);
                process.nextTick(step);
            });
        };

        process.nextTick(step);
    });
}

/**
 * Upgrade our cache database
 */
function UpgradeDB(db) {
    return new Promise((resolve, reject) => {
        // perform any database upgrade work here
        db.get("SELECT value FROM meta WHERE key = 'version'", (err, data) => {
            if (err) return reject(err);

            let version = 0;
            if (data && data.value) {
                version = Number(data.value);
            }

            if (version > currentDBVersion) {
                /* eslint-disable no-console */
                console.error(`!!!!!\nThemeparks database version is ${version}, which is higher than expected ${currentDBVersion}.\nThere may be unexpected issues running themeparks library.\n!!!!!`);
                /* eslint-enable no-console */

                return resolve();
            }

            // check if we're already at the latest DB version
            if (version == currentDBVersion) {
                return resolve();
            }

            DebugLog(`Database version out-of-date. Got: ${version}, wants: ${currentDBVersion}`);

            // bump version, upgrade database, and mark DB version number
            const step = () => {
                version++;

                // call version upgrade (if it exists)
                if (DBUpgrades[version]) {
                    DebugLog(`Updating database version to ${version}...`);
                    DBUpgrades[version](db).then(() => {
                        process.nextTick(upgradeDBVersion);
                    });
                } else {
                    // no upgrade script, update database and continue
                    process.nextTick(upgradeDBVersion);
                }
            };

            // bump version number in database, and resolve if we're at the correct version
            const upgradeDBVersion = () => {
                db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('version', ?)", [version], (err) => {
                    if (err) return reject(err);

                    if (version >= currentDBVersion) {
                        return resolve();
                    }

                    process.nextTick(step);
                });
            };

            process.nextTick(step);
        });
    });
}

const DBUpgrades = {
    1: function() {
        // version 1 doesn't need any changes to upgrade, as it's the initial version
        return Promise.resolve();
    }
};

/**
 * Clear out expired entries from our cache
 */
function ClearCache() {
    return GetDB().then(db => {
        return new Promise((resolve, reject) => {
            db.run("DELETE FROM cache WHERE expires < ?", [Math.floor(new Date().getTime() / 1000)], (err) => {
                if (err) return reject(err);
                return resolve();
            });
        });
    });
}

// clear the cache every 5 minutes
//  what is .unref()? this unreferences from Node's event queue, so any apps using themeparks won't be stopped from closing if they run out of tasks
//  we only want to do this clearout if the program is actually running, so we let this go if nothing else is going on
setInterval(ClearCache, 1000 * 60 * 5).unref();
// kick-off an initial cache clear
ClearCache();

const s_prefix = Symbol();

class Cache {
    /**
     * Create Cache object
     * This object will handle caching data for later use
     * @param {Object} [options]
     * @param {String} [options.prefix] Preface to prepend to all cache keys for this instance
     */
    constructor(options = {}) {
        // setup cache configuration
        this[s_prefix] = options.prefix || "";
    }

    /**
     * Return the sqlite3 database handle
     * @returns {Promise}
     */
    static DB() {
        return GetDB();
    }

    /**
     * Get the prefix used for this caching instance
     * @returns {String} Cache key prefix
     */
    get Prefix() {
        return this[s_prefix] + "_";
    }

    /**
     * Get a cached value
     * @param {String} key Key to request associated data for
     * @returns {Promise<Object>} Returns Promise with data (or null if not cached)
     */
    Get(key) {
        // prepend our key to restrict our request to our park's cache (avoid conflicts with other APIs)
        return this.GetGlobal(this.Prefix + key);
    }

    /**
     * Get a globally cached value
     * This is identical to Get, but the requested key will be in scope of the entire library, not just the park that requests the data
     * @param {String} key Key to request associated data for
     * @returns {Promise<Object>} Returns Promise with data (or null if not cached)
     */
    GetGlobal(key) {
        return GetDB().then(db => {
            return new Promise((resolve, reject) => {
                db.get("SELECT value FROM cache WHERE key = ? AND expires >= ?", [key, Math.floor(new Date().getTime() / 1000)], (err, data) => {
                    if (err) return reject(err);
                    if (!data) return resolve(null);

                    // parse data back into JSON
                    let JSONData;
                    try {
                        JSONData = {
                            // why in a field called data?
                            //  this makes our ParseMomentStrings update fields by reference, instead of constantly replacing the whole object tree
                            data: JSON.parse(data.value)
                        };
                    } catch (err) {
                        return resolve(null);
                    }

                    // re-parse Moment objects
                    ParseMomentStrings(JSONData);

                    return resolve(JSONData.data);
                });
            });
        });
    }

    /**
     * Set a cached value
     * @param {String} key Key to set data for
     * @param {Object} data Data to store in cache
     * @param {Number|Function} [ttl] Time this data should be cached for (in seconds)
     * @returns {Promise} Returns Promise resolving if cached data was successfully set
     */
    Set(key, data, ttl) {
        // add our prefix and pass onto the SetGlobal function
        return this.SetGlobal(this.Prefix + key, data, ttl);
    }

    /**
     * Set a cached value library-wide
     * Idential to Set, but cached at a library level instead of for the owning park
     * @param {String} key Key to set data for
     * @param {Object} data Data to store in cache
     * @param {Number|Function} [ttl] Time this data should be cached for (in seconds)
     * @returns {Promise} Returns Promise resolving if cached data was successfully set
     */
    SetGlobal(key, data, ttl) {
        return GetDB().then(db => {
            return new Promise((resolve, reject) => {
                // if we have no ttl, use our default value
                if (!ttl) {
                    ttl = Settings.DefaultCacheTime;
                }

                // process as if a promise, so we can handle ttls passed as functions, abuse Promise.all to succeed regardless of ttl variable type
                Promise.all([ttl]).then((ttl) => {
                    if (!ttl || ttl[0] == undefined) {
                        return reject("Failed to get ttl for cacher");
                    }

                    // query database
                    db.get(
                        "INSERT OR REPLACE INTO cache (key, value, expires) VALUES (?, ?, ?)", [
                            key,
                            JSON.stringify(data),
                            ttl[0] + (Math.floor(new Date().getTime() / 1000))
                        ],
                        (err) => {
                            if (err) return reject(err);
                            return resolve();
                        });
                });
            });
        });
    }

    /**
     * Wrap a get request and pass a setter to tidy up the usual get/set boilerplate
     * @param {String} key Key to get/set
     * @param {Function} setFunction Function to set data if it is missing from the cache. setFunction should return a Promise
     * @param {Number|Function} [ttl] How long cached result should last. Can be a number (seconds) or a function that will return a value
     */
    Wrap(key, setFunction, ttl) {
        return _Wrap.bind(this)(key, setFunction, this.Get, this.Set, ttl);
    }

    /**
     * Wrap a get request and pass a setter to tidy up the usual get/set boilerplate. This version sits in the global scope, rather than per-park.
     * @param {String} key Key to get/set
     * @param {Function} setFunction Function to set data if it is missing from the cache. setFunction should return a Promise
     * @param {Number|Function} [ttl] How long cached result should last. Can be a number (seconds) or a function that will return a value
     */
    WrapGlobal(key, setFunction, ttl) {
        return _Wrap.bind(this)(key, setFunction, this.GetGlobal, this.SetGlobal, ttl);
    }
}

// Internal wrap helper to not copy/paste logic for global and local-scope wraps
function _Wrap(key, setFunction, _cacheGetter, _cacheSetter, ttl) {
    return new Promise((resolve, reject) => {
        // attempt to get the requested key (note, no prefix here, it's attached in the Get function)
        //  pass success directly through to resolve
        _cacheGetter.bind(this)(key).then((data) => {
            if (data == null) {
                // if cache miss, call the setFunction to get the new value we want
                setFunction().then((dataToCache) => {
                    // store in cache
                    _cacheSetter.bind(this)(key, dataToCache, ttl).then(() => {
                        // resolve with newly cached data
                        resolve(dataToCache);
                    }, reject);
                }, reject);
            } else {
                return resolve(data);
            }
        });
    });
}

// override Moment JSON writer to store the timezone too
Moment.fn.toJSON = function() {
    // clone the object first, otherwise calling ".utc()" modifies the original object
    return "TZ|" + this.clone().utc().format("YYYY-MM-DD[T]HH:mm:ss.SSS[Z]") + "|" + this.tz();
};

// search for objects with serialized MomentJS data and restore it back into a MomentJS object
const tzRegex = /^TZ\|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}/;
const ParseMomentStrings = (obj, lvl = 0) => {
    if (!obj) return;
    if (lvl > 10) return;
    for (let k in obj) {
        if (typeof obj[k] === "string") {
            if (obj[k].match(tzRegex)) {
                const str = obj[k].split("|");
                if (str[2] !== "undefined") {
                    obj[k] = Moment.tz(str[1], str[2]);
                } else {
                    obj[k] = Moment(str[1]);
                }
            }
        } else {
            ParseMomentStrings(obj[k], lvl + 1);
        }
    }
};

// create new cache object and export it
module.exports = Cache;<commit_msg>[~] Update Cache to use new Setting name<commit_after>"use strict";

/**
 * Caching module using sqlite
 */

const sqlite3 = require("sqlite3");
const Moment = require("moment-timezone");
const Settings = require("./settings");
const DebugLog = require("./debugPrint.js");

let sqldb = null;
let pendingSetup = false;
let readyCallbacks = [];

// current database version, used for any future database scheme changes
const currentDBVersion = 1;

// get (or initialise) the cache database
function GetDB() {
    return new Promise((resolve) => {
        if (sqldb != null) {
            return resolve(sqldb);
        }

        // no setup yet, add our resolve to list of callbacks once db is ready
        readyCallbacks.push(resolve);

        // if we're already setting up elsewhere, return now and wait for our callback
        if (pendingSetup) return;
        pendingSetup = true;

        // no db setup yet, set one up
        // TODO - use filesystem
        const newdb = new sqlite3.Database(Settings.Cache, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, (err) => {
            if (err) {
                throw new Error(`Unable to initialise sqlite database: ${err}`);
            }

            if (process.env.DEBUG_DB) {
                newdb.on("trace", (query) => {
                    DebugLog("SQLite:", query);
                });
            }

            // create cache table
            SetupDB(newdb).then(() => {
                // upgrade database if needed
                return UpgradeDB(newdb);
            }).then(() => {
                // setup successful! store database reference for later and callback
                sqldb = newdb;

                // callback to all systems waiting for the database to be ready
                for (let i = 0, cb; cb = readyCallbacks[i++];) {
                    cb(sqldb);
                }
                readyCallbacks = [];
            }).catch(err => {
                throw new Error(`Failed to setup sqlite database: ${err}`);
            });
        });

    });
}

/**
 * Setup database tables
 */
function SetupDB(db) {
    return new Promise((resolve, reject) => {
        const setupQueries = [
            "CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)", // meta table, holds version data etc.
            "CREATE TABLE IF NOT EXISTS cache (key TEXT PRIMARY KEY, value TEXT, expires INTEGER NOT NULL)", // general cache table
            "CREATE TABLE IF NOT EXISTS disneyRides (id INT PRIMARY KEY, waitMinutes INTEGER, status INTEGER, document TEXT)", // couchbase-lite Disney documents
        ];

        const step = () => {
            const query = setupQueries.shift();

            if (!query) return resolve();

            db.run(query, (err) => {
                if (err) return reject(err);
                process.nextTick(step);
            });
        };

        process.nextTick(step);
    });
}

/**
 * Upgrade our cache database
 */
function UpgradeDB(db) {
    return new Promise((resolve, reject) => {
        // perform any database upgrade work here
        db.get("SELECT value FROM meta WHERE key = 'version'", (err, data) => {
            if (err) return reject(err);

            let version = 0;
            if (data && data.value) {
                version = Number(data.value);
            }

            if (version > currentDBVersion) {
                /* eslint-disable no-console */
                console.error(`!!!!!\nThemeparks database version is ${version}, which is higher than expected ${currentDBVersion}.\nThere may be unexpected issues running themeparks library.\n!!!!!`);
                /* eslint-enable no-console */

                return resolve();
            }

            // check if we're already at the latest DB version
            if (version == currentDBVersion) {
                return resolve();
            }

            DebugLog(`Database version out-of-date. Got: ${version}, wants: ${currentDBVersion}`);

            // bump version, upgrade database, and mark DB version number
            const step = () => {
                version++;

                // call version upgrade (if it exists)
                if (DBUpgrades[version]) {
                    DebugLog(`Updating database version to ${version}...`);
                    DBUpgrades[version](db).then(() => {
                        process.nextTick(upgradeDBVersion);
                    });
                } else {
                    // no upgrade script, update database and continue
                    process.nextTick(upgradeDBVersion);
                }
            };

            // bump version number in database, and resolve if we're at the correct version
            const upgradeDBVersion = () => {
                db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('version', ?)", [version], (err) => {
                    if (err) return reject(err);

                    if (version >= currentDBVersion) {
                        return resolve();
                    }

                    process.nextTick(step);
                });
            };

            process.nextTick(step);
        });
    });
}

const DBUpgrades = {
    1: function() {
        // version 1 doesn't need any changes to upgrade, as it's the initial version
        return Promise.resolve();
    }
};

/**
 * Clear out expired entries from our cache
 */
function ClearCache() {
    return GetDB().then(db => {
        return new Promise((resolve, reject) => {
            db.run("DELETE FROM cache WHERE expires < ?", [Math.floor(new Date().getTime() / 1000)], (err) => {
                if (err) return reject(err);
                return resolve();
            });
        });
    });
}

// clear the cache every 5 minutes
//  what is .unref()? this unreferences from Node's event queue, so any apps using themeparks won't be stopped from closing if they run out of tasks
//  we only want to do this clearout if the program is actually running, so we let this go if nothing else is going on
setInterval(ClearCache, 1000 * 60 * 5).unref();
// kick-off an initial cache clear
ClearCache();

const s_prefix = Symbol();

class Cache {
    /**
     * Create Cache object
     * This object will handle caching data for later use
     * @param {Object} [options]
     * @param {String} [options.prefix] Preface to prepend to all cache keys for this instance
     */
    constructor(options = {}) {
        // setup cache configuration
        this[s_prefix] = options.prefix || "";
    }

    /**
     * Return the sqlite3 database handle
     * @returns {Promise}
     */
    static DB() {
        return GetDB();
    }

    /**
     * Get the prefix used for this caching instance
     * @returns {String} Cache key prefix
     */
    get Prefix() {
        return this[s_prefix] + "_";
    }

    /**
     * Get a cached value
     * @param {String} key Key to request associated data for
     * @returns {Promise<Object>} Returns Promise with data (or null if not cached)
     */
    Get(key) {
        // prepend our key to restrict our request to our park's cache (avoid conflicts with other APIs)
        return this.GetGlobal(this.Prefix + key);
    }

    /**
     * Get a globally cached value
     * This is identical to Get, but the requested key will be in scope of the entire library, not just the park that requests the data
     * @param {String} key Key to request associated data for
     * @returns {Promise<Object>} Returns Promise with data (or null if not cached)
     */
    GetGlobal(key) {
        return GetDB().then(db => {
            return new Promise((resolve, reject) => {
                db.get("SELECT value FROM cache WHERE key = ? AND expires >= ?", [key, Math.floor(new Date().getTime() / 1000)], (err, data) => {
                    if (err) return reject(err);
                    if (!data) return resolve(null);

                    // parse data back into JSON
                    let JSONData;
                    try {
                        JSONData = {
                            // why in a field called data?
                            //  this makes our ParseMomentStrings update fields by reference, instead of constantly replacing the whole object tree
                            data: JSON.parse(data.value)
                        };
                    } catch (err) {
                        return resolve(null);
                    }

                    // re-parse Moment objects
                    ParseMomentStrings(JSONData);

                    return resolve(JSONData.data);
                });
            });
        });
    }

    /**
     * Set a cached value
     * @param {String} key Key to set data for
     * @param {Object} data Data to store in cache
     * @param {Number|Function} [ttl] Time this data should be cached for (in seconds)
     * @returns {Promise} Returns Promise resolving if cached data was successfully set
     */
    Set(key, data, ttl) {
        // add our prefix and pass onto the SetGlobal function
        return this.SetGlobal(this.Prefix + key, data, ttl);
    }

    /**
     * Set a cached value library-wide
     * Idential to Set, but cached at a library level instead of for the owning park
     * @param {String} key Key to set data for
     * @param {Object} data Data to store in cache
     * @param {Number|Function} [ttl] Time this data should be cached for (in seconds)
     * @returns {Promise} Returns Promise resolving if cached data was successfully set
     */
    SetGlobal(key, data, ttl) {
        return GetDB().then(db => {
            return new Promise((resolve, reject) => {
                // if we have no ttl, use our default value
                if (!ttl) {
                    ttl = Settings.DefaultCacheLength;
                }

                // process as if a promise, so we can handle ttls passed as functions, abuse Promise.all to succeed regardless of ttl variable type
                Promise.all([ttl]).then((ttl) => {
                    if (!ttl || ttl[0] == undefined) {
                        return reject("Failed to get ttl for cacher");
                    }

                    // query database
                    db.get(
                        "INSERT OR REPLACE INTO cache (key, value, expires) VALUES (?, ?, ?)", [
                            key,
                            JSON.stringify(data),
                            ttl[0] + (Math.floor(new Date().getTime() / 1000))
                        ],
                        (err) => {
                            if (err) return reject(err);
                            return resolve();
                        });
                });
            });
        });
    }

    /**
     * Wrap a get request and pass a setter to tidy up the usual get/set boilerplate
     * @param {String} key Key to get/set
     * @param {Function} setFunction Function to set data if it is missing from the cache. setFunction should return a Promise
     * @param {Number|Function} [ttl] How long cached result should last. Can be a number (seconds) or a function that will return a value
     */
    Wrap(key, setFunction, ttl) {
        return _Wrap.bind(this)(key, setFunction, this.Get, this.Set, ttl);
    }

    /**
     * Wrap a get request and pass a setter to tidy up the usual get/set boilerplate. This version sits in the global scope, rather than per-park.
     * @param {String} key Key to get/set
     * @param {Function} setFunction Function to set data if it is missing from the cache. setFunction should return a Promise
     * @param {Number|Function} [ttl] How long cached result should last. Can be a number (seconds) or a function that will return a value
     */
    WrapGlobal(key, setFunction, ttl) {
        return _Wrap.bind(this)(key, setFunction, this.GetGlobal, this.SetGlobal, ttl);
    }
}

// Internal wrap helper to not copy/paste logic for global and local-scope wraps
function _Wrap(key, setFunction, _cacheGetter, _cacheSetter, ttl) {
    return new Promise((resolve, reject) => {
        // attempt to get the requested key (note, no prefix here, it's attached in the Get function)
        //  pass success directly through to resolve
        _cacheGetter.bind(this)(key).then((data) => {
            if (data == null) {
                // if cache miss, call the setFunction to get the new value we want
                setFunction().then((dataToCache) => {
                    // store in cache
                    _cacheSetter.bind(this)(key, dataToCache, ttl).then(() => {
                        // resolve with newly cached data
                        resolve(dataToCache);
                    }, reject);
                }, reject);
            } else {
                return resolve(data);
            }
        });
    });
}

// override Moment JSON writer to store the timezone too
Moment.fn.toJSON = function() {
    // clone the object first, otherwise calling ".utc()" modifies the original object
    return "TZ|" + this.clone().utc().format("YYYY-MM-DD[T]HH:mm:ss.SSS[Z]") + "|" + this.tz();
};

// search for objects with serialized MomentJS data and restore it back into a MomentJS object
const tzRegex = /^TZ\|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}/;
const ParseMomentStrings = (obj, lvl = 0) => {
    if (!obj) return;
    if (lvl > 10) return;
    for (let k in obj) {
        if (typeof obj[k] === "string") {
            if (obj[k].match(tzRegex)) {
                const str = obj[k].split("|");
                if (str[2] !== "undefined") {
                    obj[k] = Moment.tz(str[1], str[2]);
                } else {
                    obj[k] = Moment(str[1]);
                }
            }
        } else {
            ParseMomentStrings(obj[k], lvl + 1);
        }
    }
};

// create new cache object and export it
module.exports = Cache;<|endoftext|>