class PromiseQueue {
  constructor(callback, options = {}) {
    this.process = callback;
    this.maxConcurrent = options.maxConcurrent || Infinity;
    this.retry = options.retry !== false;
    this.queue = [];
    this.processing = new Set();
    this.processed = new Set();
    this.numRunning = 0;
    this.runPromise = null;
    this.resolve = null;
    this.reject = null;
  }

  add(job, ...args) {
    if (this.processing.has(job)) {
      return;
    }

    if (this.runPromise && this.numRunning < this.maxConcurrent) {
      this._runJob(job, args);
    } else {
      this.queue.push([job, args]);
    }

    this.processing.add(job);
  }

  run() {
    if (this.runPromise) {
      return this.runPromise;
    }

    const runPromise = new Promise((resolve, reject) => {
      this.resolve = resolve;
      this.reject = reject;
    });

    this.runPromise = runPromise;
    this._next();

    return runPromise;
  }

  async _runJob(job, args) {
    try {
      this.numRunning++;
      await this.process(job, ...args);
      this.processing.delete(job);
      this.processed.add(job);
      this.numRunning--;
      this._next();
    } catch (err) {
      this.numRunning--;
      if (this.retry) {
        this.queue.push([job, args]);
      } else {
        this.processing.delete(job);
      }

      if (this.reject) {
        this.reject(err);
      }

      this._reset();
    }
  }

  _next() {
    if (!this.runPromise) {
      return;
    }

    if (this.queue.length > 0) {
      while (this.queue.length > 0 && this.numRunning < this.maxConcurrent) {
        this._runJob(...this.queue.shift());
      }
    } else if (this.processing.size === 0) {
      this.resolve(this.processed);
      this._reset();
    }
  }

  _reset() {
    this.processed = new Set();
    this.runPromise = null;
    this.resolve = null;
    this.reject = null;
  }
}

module.exports = PromiseQueue;


let html = fs.readFileSync(__dirname + '/dist/index.html');

keep keep keep replace keep keep keep keep


it('should insert sibling bundles at correct location in tree when optional elements are absent', async function() {

keep keep keep keep keep replace replace replace keep keep keep keep

const assert = require('assert');
const fs = require('fs');
const {bundler} = require('./utils');
const http = require('http');
const https = require('https');

describe('server', function() {
  let server;
  afterEach(function() {
    if (server) {
      server.close();
      server = null;
    }
  });

  function get(file, client = http) {
    return new Promise((resolve, reject) => {
      client.get(
        {
          hostname: 'localhost',
          port: server.address().port,
          path: file,
          rejectUnauthorized: false
        },
        res => {
          if (res.statusCode !== 200) {
            return reject(new Error('Request failed: ' + res.statusCode));
          }

          res.setEncoding('utf8');
          let data = '';
          res.on('data', c => (data += c));
          res.on('end', () => {
            resolve(data);
          });
        }
      );
    });
  }

  it('should serve files', async function() {
    let b = bundler(__dirname + '/integration/commonjs/index.js');
    server = await b.serve(0);

    let data = await get('/dist/index.js');
    assert.equal(data, fs.readFileSync(__dirname + '/dist/index.js', 'utf8'));
  });

  it('should serve a default page if the main bundle is an HTML asset', async function() {
    let b = bundler(__dirname + '/integration/html/index.html');
    server = await b.serve(0);

    let data = await get('/');
    assert.equal(data, fs.readFileSync(__dirname + '/dist/index.html', 'utf8'));

    data = await get('/foo/bar');
    assert.equal(data, fs.readFileSync(__dirname + '/dist/index.html', 'utf8'));
  });

  it('should serve a 404 if the file does not exist', async function() {
    let b = bundler(__dirname + '/integration/commonjs/index.js');
    server = await b.serve(0);

    let threw = false;
    try {
      await get('/dist/fake.js');
    } catch (err) {
      threw = true;
    }

    assert(threw);
  });

  it('should serve a 500 if the bundler errored', async function() {
    let b = bundler(__dirname + '/integration/html/index.html');
    server = await b.serve(0);

    b.errored = true;

    try {
      await get('/');
      throw new Error('GET / responded with 200');
    } catch (err) {
      assert.equal(err.message, 'Request failed: 500');
    }

    b.errored = false;
    await get('/');
  });

  it('should support HTTPS', async function() {
    let b = bundler(__dirname + '/integration/commonjs/index.js');
    server = await b.serve(0, true);

    let data = await get('/dist/index.js', https);
    assert.equal(data, fs.readFileSync(__dirname + '/dist/index.js', 'utf8'));
  });

  it('should support HTTPS via custom certificate', async function() {
    let b = bundler(__dirname + '/integration/commonjs/index.js');
    server = await b.serve(0, {
      key: __dirname + '/integration/https/private.pem',
      cert: __dirname + '/integration/https/primary.crt'
    });

    let data = await get('/dist/index.js', https);
    assert.equal(data, fs.readFileSync(__dirname + '/dist/index.js', 'utf8'));
  });

  it('should serve static assets as well as html', async function() {
    let b = bundler(__dirname + '/integration/html/index.html', {
      publicUrl: '/'
    });
    server = await b.serve(0);
    // When accessing / we should get the index page.
    let data = await get('/');
    assert.equal(data, fs.readFileSync(__dirname + '/dist/index.html', 'utf8'));
    // When accessing /hello.txt we should get txt document.
    fs.writeFileSync(__dirname + '/dist/hello.txt', 'hello');
    data = await get('/hello.txt');
    assert.equal(data, 'hello');
  });
});


const opn = require('opn');

const openInBrowser = async (url, browser) => {
  try {
    const options = typeof browser === 'string' ? {app: browser} : undefined;

    await opn(url, options);
  } catch (err) {
    console.error(`Unexpected error while opening in browser: ${browser}`);
    console.error(err);
  }
};

module.exports = openInBrowser;


keep keep keep keep replace keep keep keep keep

const assert = require('assert');
const fs = require('fs');
const {bundle} = require('./utils');
const rimraf = require('rimraf');
const promisify = require('../src/utils/promisify');
const ncp = promisify(require('ncp'));

describe('content hashing', function() {
  beforeEach(function() {
    rimraf.sync(__dirname + '/input');
  });

  it('should update content hash when content changes', async function() {
    await ncp(__dirname + '/integration/html-css', __dirname + '/input');

    await bundle(__dirname + '/input/index.html', {
      production: true
    });

    let html = fs.readFileSync(__dirname + '/dist/index.html', 'utf8');
    let filename = html.match(
      /<link rel="stylesheet" href="[/\\]{1}dist[/\\]{1}(input\.[a-f0-9]+\.css)">/
    )[1];
    assert(fs.existsSync(__dirname + '/dist/' + filename));

    fs.writeFileSync(
      __dirname + '/input/index.css',
      'body { background: green }'
    );

    await bundle(__dirname + '/input/index.html', {
      production: true
    });

    html = fs.readFileSync(__dirname + '/dist/index.html', 'utf8');
    let newFilename = html.match(
      /<link rel="stylesheet" href="[/\\]{1}dist[/\\]{1}(input\.[a-f0-9]+\.css)">/
    )[1];
    assert(fs.existsSync(__dirname + '/dist/' + newFilename));

    assert.notEqual(filename, newFilename);
  });

  it('should update content hash when raw asset changes', async function() {
    await ncp(__dirname + '/integration/import-raw', __dirname + '/input');

    await bundle(__dirname + '/input/index.js', {
      production: true
    });

    let js = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    let filename = js.match(/\/dist\/(test\.[0-9a-f]+\.txt)/)[1];
    assert(fs.existsSync(__dirname + '/dist/' + filename));

    fs.writeFileSync(__dirname + '/input/test.txt', 'hello world');

    await bundle(__dirname + '/input/index.js', {
      production: true
    });

    js = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    let newFilename = js.match(/\/dist\/(test\.[0-9a-f]+\.txt)/)[1];
    assert(fs.existsSync(__dirname + '/dist/' + newFilename));

    assert.notEqual(filename, newFilename);
  });
});


const url = require('./test.txt');

export function getRaw() {
  return url;
}


// Node 8 supports native async functions - no need to use compiled code!
module.exports = parseInt(process.versions.node, 10) < 8
  ? require('./lib/Bundler')
  : require('./src/Bundler');


const assert = require('assert');
const prettyError = require('../src/utils/prettyError');

const message = 'Error Message!';
const fileName = 'Test.js';
const codeFrame = 'frame';
const stack =
  'Error: Uh-oh. Something went wrong. Line 88. \n Oh no. Something else went wrong. Line 77 \n';

describe('prettyError', () => {
  it('should handle passing error as string', () => {
    const err = prettyError(message);

    assert.equal(err.message, message);
    assert.equal(err.stack, undefined);
  });

  it('should handle passing error as object', () => {
    const err = prettyError({message});

    assert.equal(err.message, message);
    assert.equal(err.stack, undefined);
  });

  it('should handle unknown input', () => {
    const err = prettyError(Number.NaN);

    assert(err.message.length); // non-empty error message
    assert.equal(err.stack, undefined);
  });

  it('should prepend fileName', () => {
    const err = prettyError({
      message,
      fileName
    });

    assert(err.message.startsWith(fileName));
    assert.equal(err.stack, undefined);
  });

  it('should prepend line and column location', () => {
    const err = prettyError({
      message,
      fileName,
      loc: {
        line: 1,
        column: 10
      }
    });

    assert(err.message.startsWith(`${fileName}:1:10`));
    assert.equal(err.stack, undefined);
  });

  it('should support providing a codeFrame as stack', () => {
    const err = prettyError({
      message,
      stack,
      codeFrame: codeFrame
    });

    assert.equal(err.message, message);
    assert.equal(err.stack, codeFrame);
  });

  it('should support highlightedCodeFrame when opts.color is true', () => {
    let err = prettyError(
      {
        message,
        stack,
        codeFrame: '<not>a code frame</not>',
        highlightedCodeFrame: codeFrame
      },
      {color: true}
    );

    assert.equal(err.message, message);
    assert.equal(err.stack, codeFrame);

    err = prettyError(
      {
        message,
        stack,
        codeFrame: codeFrame,
        highlightedCodeFrame: '<not>a code frame</not>'
      },
      {color: false}
    );

    assert.equal(err.message, message);
    assert.equal(err.stack, codeFrame);
  });

  it('should support stack', () => {
    const err = prettyError({
      message,
      stack
    });

    assert.equal(err.message, message);
    assert(err.stack.includes('Line'));
  });
});


it('should minify JS in production mode', async function() {

let b = await bundle(__dirname + '/integration/uglify/index.js', {

keep keep keep keep replace replace keep keep keep keep

assert(fs.existsSync(__dirname + '/dist/' + output()));


it('should serve files', async function() {

let b = bundler(__dirname + '/integration/commonjs/index.js');

assert.equal(data, fs.readFileSync(__dirname + '/dist/index.js', 'utf8'));

it('should serve a default page if the main bundle is an HTML asset', async function() {

let b = bundler(__dirname + '/integration/html/index.html');

keep keep keep keep keep replace keep keep keep keep keep


const Local = require('./Local');

export function count() {
  let local = new Local(1, 2);
  return local.a + local.b;
}


const a = 1;
const b = 2;

export default () => a + b;


const Asset = require('../Asset');

class WebManifestAsset extends Asset {
  constructor(name, pkg, options) {
    super(name, pkg, options);
    this.type = 'webmanifest';
  }

  parse(content) {
    return JSON.parse(content);
  }

  collectDependencies() {
    if (Array.isArray(this.ast.icons)) {
      for (let icon of this.ast.icons) {
        icon.src = this.addURLDependency(icon.src);
      }
    }

    if (Array.isArray(this.ast.screenshots)) {
      for (let shot of this.ast.screenshots) {
        shot.src = this.addURLDependency(shot.src);
      }
    }

    if (this.ast.serviceworker && this.ast.serviceworker.src) {
      this.ast.serviceworker.src = this.addURLDependency(
        this.ast.serviceworker.src
      );
    }
  }

  generate() {
    return {
      webmanifest: JSON.stringify(this.ast)
    };
  }
}

module.exports = WebManifestAsset;


let threw = false;
try {
await get('/dist/fake.js');
} catch (err) {
threw = true;
}


const Asset = require('../Asset');
const localRequire = require('../utils/localRequire');
const path = require('path');
const promisify = require('../utils/promisify');
const Resolver = require('../Resolver');

class GLSLAsset extends Asset {
  constructor(name, pkg, options) {
    super(name, pkg, options);
    this.type = 'js';
  }

  async parse() {
    const glslifyDeps = await localRequire('glslify-deps', this.name);

    // Use the Parcel resolver rather than the default glslify one.
    // This adds support for parcel features like alises, and tilde paths.
    const resolver = new Resolver({
      extensions: ['.glsl', '.vert', '.frag'],
      rootDir: this.options.rootDir
    });

    // Parse and collect dependencies with glslify-deps
    let cwd = path.dirname(this.name);
    let depper = glslifyDeps({
      cwd,
      resolve: async (target, opts, next) => {
        try {
          let res = await resolver.resolve(
            target,
            path.join(opts.basedir, 'index')
          );
          next(null, res.path);
        } catch (err) {
          next(err);
        }
      }
    });

    return await promisify(depper.inline.bind(depper))(this.contents, cwd);
  }

  collectDependencies() {
    for (let dep of this.ast) {
      if (!dep.entry) {
        this.addDependency(dep.file, {includedInParent: true});
      }
    }
  }

  async generate() {
    // Generate the bundled glsl file
    const glslifyBundle = await localRequire('glslify-bundle', this.name);
    let glsl = glslifyBundle(this.ast);

    return {
      js: `module.exports=${JSON.stringify(glsl)};`
    };
  }
}

module.exports = GLSLAsset;


it('should support HTTPS', async function() {

let data = await get('/dist/index.js', https);

it('should support HTTPS via custom certificate', async function() {

keep keep keep keep keep replace keep keep keep keep

let data = await get('/index.js', https);

const assert = require('assert');
const fs = require('fs');
const {bundle, assertBundleTree} = require('./utils');

describe('html', function() {
  it('should support bundling HTML', async function() {
    let b = await bundle(__dirname + '/integration/html/index.html');

    assertBundleTree(b, {
      name: 'index.html',
      assets: ['index.html'],
      childBundles: [
        {
          type: 'png',
          assets: ['100x100.png'],
          childBundles: []
        },
        {
          type: 'svg',
          assets: ['icons.svg'],
          childBundles: []
        },
        {
          type: 'css',
          assets: ['index.css'],
          childBundles: []
        },
        {
          type: 'js',
          assets: ['index.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        },
        {
          type: 'html',
          assets: ['other.html'],
          childBundles: [
            {
              type: 'css',
              assets: ['index.css'],
              childBundles: []
            },
            {
              type: 'js',
              assets: ['index.js'],
              childBundles: [
                {
                  type: 'map'
                }
              ]
            }
          ]
        }
      ]
    });

    let files = fs.readdirSync(__dirname + '/dist');
    let html = fs.readFileSync(__dirname + '/dist/index.html');
    for (let file of files) {
      let ext = file.match(/\.([0-9a-z]+)(?:[?#]|$)/i)[0];
      if (file !== 'index.html' && ext !== '.map') {
        assert(html.includes(file));
      }
    }
  });

  it('should find href attr when not first', async function() {
    let b = await bundle(__dirname + '/integration/html-attr-order/index.html');

    assertBundleTree(b, {
      name: 'index.html',
      assets: ['index.html'],
      childBundles: [
        {
          type: 'html',
          assets: ['other.html'],
          childBundles: []
        }
      ]
    });
  });

  it('should support transforming HTML with posthtml', async function() {
    let b = await bundle(__dirname + '/integration/posthtml/index.html');

    assertBundleTree(b, {
      name: 'index.html',
      assets: ['index.html'],
      childBundles: []
    });

    let html = fs.readFileSync(__dirname + '/dist/index.html');
    assert(html.includes('
Other page
'));
  });

  it('should find assets inside posthtml', async function() {
    let b = await bundle(__dirname + '/integration/posthtml-assets/index.html');

    assertBundleTree(b, {
      name: 'index.html',
      assets: ['index.html'],
      childBundles: [
        {
          type: 'js',
          assets: ['index.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        }
      ]
    });
  });

  it('should insert sibling CSS bundles for JS files in the HEAD', async function() {
    let b = await bundle(__dirname + '/integration/html-css/index.html');

    assertBundleTree(b, {
      name: 'index.html',
      assets: ['index.html'],
      childBundles: [
        {
          type: 'js',
          assets: ['index.js', 'index.css'],
          childBundles: [
            {
              type: 'css',
              assets: ['index.css'],
              childBundles: []
            },
            {
              type: 'map'
            }
          ]
        }
      ]
    });

    let html = fs.readFileSync(__dirname + '/dist/index.html');
    assert(
      /<link rel="stylesheet" href="[/\\]{1}dist[/\\]{1}html-css\.[a-f0-9]+\.css">/.test(
        html
      )
    );
  });

  it('should insert sibling bundles before body element if no HEAD', async function() {
    let b = await bundle(__dirname + '/integration/html-css-head/index.html');

    assertBundleTree(b, {
      name: 'index.html',
      assets: ['index.html'],
      childBundles: [
        {
          type: 'js',
          assets: ['index.js', 'index.css'],
          childBundles: [
            {
              type: 'css',
              assets: ['index.css'],
              childBundles: []
            },
            {
              type: 'map'
            }
          ]
        }
      ]
    });

    let html = fs.readFileSync(__dirname + '/dist/index.html');
    assert(
      /\s*<link rel="stylesheet" href="[/\\]{1}dist[/\\]{1}html-css-head\.[a-f0-9]+\.css">\s*/.test(
        html
      )
    );
  });

  it('should insert sibling JS bundles for CSS files in the HEAD', async function() {
    let b = await bundle(__dirname + '/integration/html-css-js/index.html', {
      hmr: true
    });

    assertBundleTree(b, {
      name: 'index.html',
      assets: ['index.html'],
      childBundles: [
        {
          type: 'css',
          assets: ['index.css'],
          childBundles: [
            {
              type: 'map'
            },
            {
              type: 'js',
              assets: [
                'index.css',
                'bundle-url.js',
                'css-loader.js',
                'hmr-runtime.js'
              ],
              childBundles: []
            }
          ]
        }
      ]
    });

    let html = fs.readFileSync(__dirname + '/dist/index.html');
    assert(
      /')
    );

    // minifySvg is false
    assert(
      html.includes(
        '<svg version="1.1" baseprofile="full" width="300" height="200" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="red"></rect><circle cx="150" cy="100" r="80" fill="green"></circle><text x="150" y="125" font-size="60" text-anchor="middle" fill="white">SVG</text></svg>'
      )
    );
  });

  it('should not prepend the public path to assets with remote URLs', async function() {
    await bundle(__dirname + '/integration/html/index.html');

    let html = fs.readFileSync(__dirname + '/dist/index.html', 'utf8');
    assert(
      html.includes('')
    );
  });

  it('should not prepend the public path to hash links', async function() {
    await bundle(__dirname + '/integration/html/index.html');

    let html = fs.readFileSync(__dirname + '/dist/index.html', 'utf8');
    assert(html.includes(''));
  });

  it('should detect virtual paths', async function() {
    let b = await bundle(
      __dirname + '/integration/html-virtualpath/index.html'
    );

    assertBundleTree(b, {
      name: 'index.html',
      assets: ['index.html'],
      childBundles: [
        {
          type: 'html',
          assets: ['other.html'],
          childBundles: []
        }
      ]
    });
  });

  it('should not update root/main file in the bundles', async function() {
    await bundle(__dirname + '/integration/html-root/index.html');

    let files = fs.readdirSync(__dirname + '/dist');

    for (let file of files) {
      if (file !== 'index.html' && file.endsWith('.html')) {
        let html = fs.readFileSync(__dirname + '/dist/' + file);
        assert(html.includes('index.html'));
      }
    }
  });

  it('should conserve the spacing in the HTML tags', async function() {
    await bundle(__dirname + '/integration/html/index.html', {
      production: true
    });

    let html = fs.readFileSync(__dirname + '/dist/index.html', 'utf8');
    assert(/hello<\/i> world<\/i>/.test(html));
  });

  it('should support child bundles of different types', async function() {
    let b = await bundle(
      __dirname + '/integration/child-bundle-different-types/index.html'
    );

    assertBundleTree(b, {
      name: 'index.html',
      assets: ['index.html'],
      childBundles: [
        {
          type: 'js',
          assets: ['main.js', 'util.js', 'other.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        },
        {
          type: 'html',
          assets: ['other.html'],
          childBundles: [
            {
              type: 'js',
              assets: ['index.js', 'util.js', 'other.js'],
              childBundles: [
                {
                  type: 'map'
                }
              ]
            }
          ]
        }
      ]
    });
  });

  it('should support circular dependencies', async function() {
    let b = await bundle(__dirname + '/integration/circular/index.html');

    assertBundleTree(b, {
      name: 'index.html',
      assets: ['index.html'],
      childBundles: [
        {
          type: 'html',
          assets: ['about.html'],
          childBundles: [
            {
              type: 'js',
              assets: ['about.js', 'index.js'],
              childBundles: [
                {
                  type: 'map'
                }
              ]
            },
            {
              type: 'html',
              assets: ['test.html'],
              childBundles: []
            }
          ]
        },
        {
          type: 'js',
          assets: ['about.js', 'index.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        }
      ]
    });
  });

  it('should support bundling HTM', async function() {
    let b = await bundle(__dirname + '/integration/htm-extension/index.htm');

    assertBundleTree(b, {
      name: 'index.html',
      assets: ['index.htm'],
      type: 'html',
      childBundles: [
        {
          type: 'js',
          assets: ['index.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        }
      ]
    });
  });

  it('should detect srcset attribute', async function() {
    let b = await bundle(__dirname + '/integration/html-srcset/index.html');

    assertBundleTree(b, {
      name: 'index.html',
      assets: ['index.html'],
      childBundles: [
        {
          type: 'png',
          assets: ['100x100.png'],
          childBundles: []
        },
        {
          type: 'png',
          assets: ['200x200.png'],
          childBundles: []
        },
        {
          type: 'png',
          assets: ['300x300.png'],
          childBundles: []
        }
      ]
    });
  });

  it('should detect srcset attribute of source element', async function() {
    let b = await bundle(
      __dirname + '/integration/html-source-srcset/index.html'
    );

    assertBundleTree(b, {
      name: 'index.html',
      assets: ['index.html'],
      childBundles: [
        {
          type: 'png',
          assets: ['100x100.png'],
          childBundles: []
        },
        {
          type: 'png',
          assets: ['200x200.png'],
          childBundles: []
        },
        {
          type: 'png',
          assets: ['300x300.png'],
          childBundles: []
        }
      ]
    });
  });

  it('should support webmanifest', async function() {
    let b = await bundle(__dirname + '/integration/webmanifest/index.html');

    assertBundleTree(b, {
      name: 'index.html',
      assets: ['index.html'],
      childBundles: [
        {
          type: 'webmanifest',
          assets: ['manifest.webmanifest'],
          childBundles: [
            {
              type: 'txt',
              assets: ['some.txt'],
              childBundles: []
            }
          ]
        }
      ]
    });
  });

  it('should bundle svg files correctly', async function() {
    let b = await bundle(__dirname + '/integration/html-svg/index.html');

    assertBundleTree(b, {
      name: 'index.html',
      assets: ['index.html'],
      childBundles: [
        {
          type: 'svg',
          assets: ['file.svg']
        }
      ]
    });
  });
});


{
  user(id: 5) {
    ...UserFragment
  }
}
fragment UserFragment on User {
  firstName
  lastName
}


const required = require('./pkg-both')

if(required.test() !== 'pkg-es6-module') {
    throw new Error('Invalid module')
}

export const test = required.test


cert: __dirname + '/integration/https/primary.crt'

it('should serve static assets as well as html', async function() {

assert.equal(data, fs.readFileSync(__dirname + '/dist/index.js', 'utf8'));

it('should support setting a public url', async function() {

let b = bundler(__dirname + '/integration/commonjs/index.js', {

const assert = require('assert');
const fs = require('fs');
const path = require('path');
const {bundle, run, assertBundleTree} = require('./utils');
const {mkdirp} = require('../src/utils/fs');

describe('javascript', function() {
  it('should produce a basic JS bundle with CommonJS requires', async function() {
    let b = await bundle(__dirname + '/integration/commonjs/index.js');

    assert.equal(b.assets.size, 8);
    assert.equal(b.childBundles.size, 1);

    let output = run(b);
    assert.equal(typeof output, 'function');
    assert.equal(output(), 3);
  });

  it('should produce a basic JS bundle with ES6 imports', async function() {
    let b = await bundle(__dirname + '/integration/es6/index.js');

    assert.equal(b.assets.size, 8);
    assert.equal(b.childBundles.size, 1);

    let output = run(b);
    assert.equal(typeof output, 'object');
    assert.equal(typeof output.default, 'function');
    assert.equal(output.default(), 3);
  });

  it('should bundle node_modules on --target=browser', async function() {
    let b = await bundle(__dirname + '/integration/node_require/main.js', {
      target: 'browser'
    });

    assertBundleTree(b, {
      name: 'main.js',
      assets: ['main.js', 'local.js', 'index.js']
    });

    let output = run(b);
    assert.equal(typeof output, 'function');
    assert.equal(output(), 3);
  });

  it('should not bundle node_modules on --target=node', async function() {
    let b = await bundle(__dirname + '/integration/node_require/main.js', {
      target: 'node'
    });

    assertBundleTree(b, {
      name: 'main.js',
      assets: ['main.js', 'local.js']
    });

    await mkdirp(__dirname + '/dist/node_modules/testmodule');
    fs.writeFileSync(
      __dirname + '/dist/node_modules/testmodule/index.js',
      'exports.a = 5;'
    );

    let output = run(b);
    assert.equal(typeof output, 'function');
    assert.equal(output(), 7);
  });

  it('should not bundle node_modules on --target=electron', async function() {
    let b = await bundle(__dirname + '/integration/node_require/main.js', {
      target: 'electron'
    });

    assertBundleTree(b, {
      name: 'main.js',
      assets: ['main.js', 'local.js']
    });

    await mkdirp(__dirname + '/dist/node_modules/testmodule');
    fs.writeFileSync(
      __dirname + '/dist/node_modules/testmodule/index.js',
      'exports.a = 5;'
    );

    let output = run(b);
    assert.equal(typeof output, 'function');
    assert.equal(output(), 7);
  });

  it('should produce a JS bundle with default exports and no imports', async function() {
    let b = await bundle(__dirname + '/integration/es6-default-only/index.js');

    assert.equal(b.assets.size, 1);
    assert.equal(b.childBundles.size, 1);

    let output = run(b);
    assert.equal(typeof output, 'object');
    assert.equal(typeof output.default, 'function');
    assert.equal(output.default(), 3);
  });

  it('should split bundles when a dynamic import is used', async function() {
    let b = await bundle(__dirname + '/integration/dynamic/index.js');

    assertBundleTree(b, {
      name: 'index.js',
      assets: ['index.js', 'bundle-loader.js', 'bundle-url.js', 'js-loader.js'],
      childBundles: [
        {
          type: 'map'
        },
        {
          assets: ['local.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        }
      ]
    });

    let output = run(b);
    assert.equal(typeof output, 'function');
    assert.equal(await output(), 3);
  });

  it('should support bundling workers', async function() {
    let b = await bundle(__dirname + '/integration/workers/index.js');

    assertBundleTree(b, {
      name: 'index.js',
      assets: ['index.js'],
      childBundles: [
        {
          type: 'map'
        },
        {
          assets: ['service-worker.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        },
        {
          assets: ['worker.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        }
      ]
    });
  });

  it('should dynamic import files which import raw files', async function() {
    let b = await bundle(
      __dirname + '/integration/dynamic-references-raw/index.js'
    );

    assertBundleTree(b, {
      name: 'index.js',
      assets: ['index.js', 'bundle-loader.js', 'bundle-url.js', 'js-loader.js'],
      childBundles: [
        {
          type: 'map'
        },
        {
          assets: ['local.js', 'test.txt'],
          childBundles: [
            {
              type: 'map'
            },
            {
              assets: ['test.txt']
            }
          ]
        }
      ]
    });

    let output = run(b);
    assert.equal(typeof output, 'function');
    assert.equal(await output(), 3);
  });

  it('should return all exports as an object when using ES modules', async function() {
    let b = await bundle(__dirname + '/integration/dynamic-esm/index.js');

    assertBundleTree(b, {
      name: 'index.js',
      assets: ['index.js', 'bundle-loader.js', 'bundle-url.js', 'js-loader.js'],
      childBundles: [
        {
          type: 'map'
        },
        {
          assets: ['local.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        }
      ]
    });

    let output = run(b).default;
    assert.equal(typeof output, 'function');
    assert.equal(await output(), 3);
  });

  it('should hoist common dependencies into a parent bundle', async function() {
    let b = await bundle(__dirname + '/integration/dynamic-hoist/index.js');

    assertBundleTree(b, {
      name: 'index.js',
      assets: [
        'index.js',
        'common.js',
        'common-dep.js',
        'bundle-loader.js',
        'bundle-url.js',
        'js-loader.js'
      ],
      childBundles: [
        {
          assets: ['a.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        },
        {
          assets: ['b.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        },
        {
          type: 'map'
        }
      ]
    });

    let output = run(b);
    assert.equal(typeof output, 'function');
    assert.equal(await output(), 7);
  });

  it('should support requiring JSON files', async function() {
    let b = await bundle(__dirname + '/integration/json/index.js');

    assertBundleTree(b, {
      name: 'index.js',
      assets: ['index.js', 'local.json'],
      childBundles: [
        {
          type: 'map'
        }
      ]
    });

    let output = run(b);
    assert.equal(typeof output, 'function');
    assert.equal(output(), 3);
  });

  it('should support requiring JSON5 files', async function() {
    let b = await bundle(__dirname + '/integration/json5/index.js');

    assertBundleTree(b, {
      name: 'index.js',
      assets: ['index.js', 'local.json5'],
      childBundles: [
        {
          type: 'map'
        }
      ]
    });

    let output = run(b);
    assert.equal(typeof output, 'function');
    assert.equal(output(), 3);
  });

  it('should support importing a URL to a raw asset', async function() {
    let b = await bundle(__dirname + '/integration/import-raw/index.js');

    assertBundleTree(b, {
      name: 'index.js',
      assets: ['index.js', 'test.txt'],
      childBundles: [
        {
          type: 'map'
        },
        {
          type: 'txt',
          assets: ['test.txt'],
          childBundles: []
        }
      ]
    });

    let output = run(b);
    assert.equal(typeof output, 'function');
    assert(/^\/dist\/test\.[0-9a-f]+\.txt$/.test(output()));
    assert(fs.existsSync(__dirname + output()));
  });

  it('should minify JS in production mode', async function() {
    let b = await bundle(__dirname + '/integration/uglify/index.js', {
      production: true
    });

    let output = run(b);
    assert.equal(typeof output, 'function');
    assert.equal(output(), 3);

    let js = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(!js.includes('local.a'));
  });

  it('should use uglify config', async function() {
    await bundle(__dirname + '/integration/uglify-config/index.js', {
      production: true
    });

    let js = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(!js.includes('console.log'));
    assert(!js.includes('// This is a comment'));
  });

  it('should insert global variables when needed', async function() {
    let b = await bundle(__dirname + '/integration/globals/index.js');

    let output = run(b);
    assert.deepEqual(output(), {
      dir: path.join(__dirname, '/integration/globals'),
      file: path.join(__dirname, '/integration/globals/index.js'),
      buf: new Buffer('browser').toString('base64'),
      global: true
    });
  });

  it('should insert environment variables', async function() {
    let b = await bundle(__dirname + '/integration/env/index.js');

    let output = run(b);
    assert.equal(output(), 'test:test');
  });

  it('should insert environment variables from a file', async function() {
    let b = await bundle(__dirname + '/integration/env-file/index.js');

    let output = run(b);
    assert.equal(output, 'bartest');
  });

  it('should support adding implicit dependencies', async function() {
    let b = await bundle(__dirname + '/integration/json/index.js', {
      delegate: {
        getImplicitDependencies(asset) {
          if (asset.basename === 'index.js') {
            return [{name: '../css/index.css'}];
          }
        }
      }
    });

    assertBundleTree(b, {
      name: 'index.js',
      assets: ['index.js', 'local.json', 'index.css'],
      childBundles: [
        {
          type: 'css',
          assets: ['index.css']
        },
        {
          type: 'map'
        }
      ]
    });

    let output = run(b);
    assert.equal(typeof output, 'function');
    assert.equal(output(), 3);
  });

  it('should support requiring YAML files', async function() {
    let b = await bundle(__dirname + '/integration/yaml/index.js');

    assertBundleTree(b, {
      name: 'index.js',
      assets: ['index.js', 'local.yaml'],
      childBundles: [
        {
          type: 'map'
        }
      ]
    });

    let output = run(b);
    assert.equal(typeof output, 'function');
    assert.equal(output(), 3);
  });

  it('should support requiring TOML files', async function() {
    let b = await bundle(__dirname + '/integration/toml/index.js');

    assertBundleTree(b, {
      name: 'index.js',
      assets: ['index.js', 'local.toml'],
      childBundles: [
        {
          type: 'map'
        }
      ]
    });

    let output = run(b);
    assert.equal(typeof output, 'function');
    assert.equal(output(), 3);
  });

  it('should support requiring CoffeeScript files', async function() {
    let b = await bundle(__dirname + '/integration/coffee/index.js');

    assertBundleTree(b, {
      name: 'index.js',
      assets: ['index.js', 'local.coffee'],
      childBundles: [
        {
          type: 'map'
        }
      ]
    });

    let output = run(b);
    assert.equal(typeof output, 'function');
    assert.equal(output(), 3);
  });

  it('should resolve the browser field before main', async function() {
    let b = await bundle(__dirname + '/integration/resolve-entries/browser.js');

    assertBundleTree(b, {
      name: 'browser.js',
      assets: ['browser.js', 'browser-module.js'],
      childBundles: [
        {
          type: 'map'
        }
      ]
    });

    let output = run(b);

    assert.equal(typeof output.test, 'function');
    assert.equal(output.test(), 'pkg-browser');
  });

  it('should resolve advanced browser resolution', async function() {
    let b = await bundle(
      __dirname + '/integration/resolve-entries/browser-multiple.js'
    );

    assertBundleTree(b, {
      name: 'browser-multiple.js',
      assets: ['browser-multiple.js', 'projected-module.js'],
      childBundles: [
        {
          type: 'map'
        }
      ]
    });

    let output = run(b);

    assert.equal(typeof output.test, 'function');
    assert.equal(output.test(), 'pkg-browser-multiple');
  });

  it('should resolve the module field before main', async function() {
    let b = await bundle(
      __dirname + '/integration/resolve-entries/module-field.js'
    );

    assertBundleTree(b, {
      name: 'module-field.js',
      assets: ['module-field.js', 'es6.module.js'],
      childBundles: [
        {
          type: 'map'
        }
      ]
    });

    let output = run(b);

    assert.equal(typeof output.test, 'function');
    assert.equal(output.test(), 'pkg-es6-module');
  });

  it('should resolve the jsnext:main field before main', async function() {
    let b = await bundle(
      __dirname + '/integration/resolve-entries/jsnext-field.js'
    );

    assertBundleTree(b, {
      name: 'jsnext-field.js',
      assets: ['jsnext-field.js', 'jsnext.module.js'],
      childBundles: [
        {
          type: 'map'
        }
      ]
    });

    let output = run(b);

    assert.equal(typeof output.test, 'function');
    assert.equal(output.test(), 'pkg-jsnext-module');
  });

  it('should resolve the module field before jsnext:main', async function() {
    let b = await bundle(
      __dirname + '/integration/resolve-entries/both-fields.js'
    );

    assertBundleTree(b, {
      name: 'both-fields.js',
      assets: ['both-fields.js', 'es6.module.js'],
      childBundles: [
        {
          type: 'map'
        }
      ]
    });

    let output = run(b);

    assert.equal(typeof output.test, 'function');
    assert.equal(output.test(), 'pkg-es6-module');
  });

  it('should resolve the main field', async function() {
    let b = await bundle(
      __dirname + '/integration/resolve-entries/main-field.js'
    );

    assertBundleTree(b, {
      name: 'main-field.js',
      assets: ['main-field.js', 'main.js'],
      childBundles: [
        {
          type: 'map'
        }
      ]
    });

    let output = run(b);

    assert.equal(typeof output.test, 'function');
    assert.equal(output.test(), 'pkg-main-module');
  });

  it('should minify JSON files', async function() {
    await bundle(__dirname + '/integration/uglify-json/index.json', {
      production: true
    });

    let json = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(json.includes('{test:"test"}'));
  });

  it('should minify JSON5 files', async function() {
    await bundle(__dirname + '/integration/uglify-json5/index.json5', {
      production: true
    });

    let json = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(json.includes('{test:"test"}'));
  });

  it('should minify YAML for production', async function() {
    await bundle(__dirname + '/integration/yaml/index.js', {
      production: true
    });

    let json = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(json.includes('{a:1,b:{c:2}}'));
  });

  it('should minify TOML for production', async function() {
    await bundle(__dirname + '/integration/toml/index.js', {
      production: true
    });

    let json = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(json.includes('{a:1,b:{c:2}}'));
  });

  it('should support compiling with babel using .babelrc config', async function() {
    await bundle(__dirname + '/integration/babel/index.js');

    let file = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(file.includes('class Foo {}'));
    assert(file.includes('class Bar {}'));
  });

  it('should compile with babel with default engines if no config', async function() {
    await bundle(__dirname + '/integration/babel-default/index.js');

    let file = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(!file.includes('class Foo {}'));
    assert(!file.includes('class Bar {}'));
  });

  it('should support compiling with babel using browserlist', async function() {
    await bundle(__dirname + '/integration/babel-browserslist/index.js');

    let file = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(!file.includes('class Foo {}'));
    assert(!file.includes('class Bar {}'));
  });

  it('should not compile node_modules by default', async function() {
    await bundle(__dirname + '/integration/babel-node-modules/index.js');

    let file = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(file.includes('class Foo {}'));
    assert(!file.includes('class Bar {}'));
  });

  it('should compile node_modules if legacy browserify options are found', async function() {
    await bundle(
      __dirname + '/integration/babel-node-modules-browserify/index.js'
    );

    let file = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(!file.includes('class Foo {}'));
    assert(!file.includes('class Bar {}'));
  });

  it('should compile node_modules with browserslist to app target', async function() {
    await bundle(
      __dirname + '/integration/babel-node-modules-browserslist/index.js'
    );

    let file = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(!file.includes('class Foo {}'));
    assert(!file.includes('class Bar {}'));
  });

  it('should support compiling JSX', async function() {
    await bundle(__dirname + '/integration/jsx/index.jsx');

    let file = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(file.includes('React.createElement("div"'));
  });

  it('should support compiling JSX in JS files with React dependency', async function() {
    await bundle(__dirname + '/integration/jsx-react/index.js');

    let file = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(file.includes('React.createElement("div"'));
  });

  it('should support compiling JSX in JS files with Preact dependency', async function() {
    await bundle(__dirname + '/integration/jsx-preact/index.js');

    let file = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(file.includes('h("div"'));
  });

  it('should support compiling JSX in JS files with Nerv dependency', async function() {
    await bundle(__dirname + '/integration/jsx-nervjs/index.js');

    let file = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(file.includes('Nerv.createElement("div"'));
  });

  it('should support compiling JSX in JS files with Hyperapp dependency', async function() {
    await bundle(__dirname + '/integration/jsx-hyperapp/index.js');

    let file = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(file.includes('h("div"'));
  });

  it('should support optional dependencies in try...catch blocks', async function() {
    let b = await bundle(__dirname + '/integration/optional-dep/index.js');

    assertBundleTree(b, {
      name: 'index.js',
      assets: ['index.js'],
      childBundles: [
        {
          type: 'map'
        }
      ]
    });

    let output = run(b);

    let err = new Error('Cannot find module "optional-dep"');
    err.code = 'MODULE_NOT_FOUND';

    assert.deepEqual(output, err);
  });
});


var local = require('./local.toml');

module.exports = function () {
  return local.a + local.b.c;
};


const assert = require('assert');
const fs = require('fs');
const path = require('path');
const {bundler, run, assertBundleTree, sleep, nextBundle} = require('./utils');
const rimraf = require('rimraf');
const promisify = require('../src/utils/promisify');
const ncp = promisify(require('ncp'));

describe('watcher', function() {
  let b;
  beforeEach(function() {
    rimraf.sync(__dirname + '/input');
  });

  afterEach(function() {
    if (b) {
      b.stop();
    }
  });

  it('should rebuild on file change', async function() {
    await ncp(__dirname + '/integration/commonjs', __dirname + '/input');

    b = bundler(__dirname + '/input/index.js', {watch: true});
    let bundle = await b.bundle();
    let output = run(bundle);
    assert.equal(output(), 3);

    fs.writeFileSync(
      __dirname + '/input/local.js',
      'exports.a = 5; exports.b = 5;'
    );

    bundle = await nextBundle(b);
    output = run(bundle);
    assert.equal(output(), 10);
  });

  it('should re-generate bundle tree when files change', async function() {
    await ncp(__dirname + '/integration/dynamic-hoist', __dirname + '/input');

    b = bundler(__dirname + '/input/index.js', {watch: true});
    let bundle = await b.bundle();

    assertBundleTree(bundle, {
      name: 'index.js',
      assets: [
        'index.js',
        'common.js',
        'common-dep.js',
        'bundle-loader.js',
        'bundle-url.js',
        'js-loader.js'
      ],
      childBundles: [
        {
          assets: ['a.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        },
        {
          assets: ['b.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        },
        {
          type: 'map'
        }
      ]
    });

    let output = run(bundle);
    assert.equal(await output(), 7);

    // change b.js so that it no longer depends on common.js.
    // This should cause common.js and dependencies to no longer be hoisted to the root bundle.
    fs.writeFileSync(__dirname + '/input/b.js', 'module.exports = 5;');

    bundle = await nextBundle(b);
    assertBundleTree(bundle, {
      name: 'index.js',
      assets: ['index.js', 'bundle-loader.js', 'bundle-url.js', 'js-loader.js'],
      childBundles: [
        {
          assets: ['a.js', 'common.js', 'common-dep.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        },
        {
          assets: ['b.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        },
        {
          type: 'map'
        }
      ]
    });

    output = run(bundle);
    assert.equal(await output(), 8);
  });

  it('should only re-package bundles that changed', async function() {
    await ncp(__dirname + '/integration/dynamic-hoist', __dirname + '/input');
    b = bundler(__dirname + '/input/index.js', {watch: true});

    await b.bundle();
    let mtimes = fs
      .readdirSync(__dirname + '/dist')
      .map(
        f => (fs.statSync(__dirname + '/dist/' + f).mtime.getTime() / 1000) | 0
      );

    await sleep(1100); // mtime only has second level precision
    fs.writeFileSync(
      __dirname + '/input/b.js',
      'module.exports = require("./common")'
    );

    await nextBundle(b);
    let newMtimes = fs
      .readdirSync(__dirname + '/dist')
      .map(
        f => (fs.statSync(__dirname + '/dist/' + f).mtime.getTime() / 1000) | 0
      );
    assert.deepEqual(mtimes.sort().slice(0, 2), newMtimes.sort().slice(0, 2));
    assert.notEqual(mtimes[mtimes.length - 1], newMtimes[newMtimes.length - 1]);
  });

  it('should unload assets that are orphaned', async function() {
    await ncp(__dirname + '/integration/dynamic-hoist', __dirname + '/input');
    b = bundler(__dirname + '/input/index.js', {watch: true});

    let bundle = await b.bundle();
    assertBundleTree(bundle, {
      name: 'index.js',
      assets: [
        'index.js',
        'common.js',
        'common-dep.js',
        'bundle-loader.js',
        'bundle-url.js',
        'js-loader.js'
      ],
      childBundles: [
        {
          assets: ['a.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        },
        {
          assets: ['b.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        },
        {
          type: 'map'
        }
      ]
    });

    let output = run(bundle);
    assert.equal(await output(), 7);

    assert(b.loadedAssets.has(path.join(__dirname, '/input/common-dep.js')));

    // Get rid of common-dep.js
    fs.writeFileSync(__dirname + '/input/common.js', 'module.exports = 5;');

    bundle = await nextBundle(b);
    assertBundleTree(bundle, {
      name: 'index.js',
      assets: [
        'index.js',
        'common.js',
        'bundle-loader.js',
        'bundle-url.js',
        'js-loader.js'
      ],
      childBundles: [
        {
          assets: ['a.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        },
        {
          assets: ['b.js'],
          childBundles: [
            {
              type: 'map'
            }
          ]
        },
        {
          type: 'map'
        }
      ]
    });

    output = run(bundle);
    assert.equal(await output(), 13);

    assert(!b.loadedAssets.has(path.join(__dirname, '/input/common-dep.js')));
  });

  it('should recompile all assets when a config file changes', async function() {
    await ncp(__dirname + '/integration/babel', __dirname + '/input');
    b = bundler(__dirname + '/input/index.js', {watch: true});

    await b.bundle();
    let file = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(file.includes('class Foo {}'));
    assert(file.includes('class Bar {}'));

    // Change babelrc, should recompile both files
    let babelrc = JSON.parse(
      fs.readFileSync(__dirname + '/input/.babelrc', 'utf8')
    );
    babelrc.presets[0][1].targets.browsers.push('IE >= 11');
    fs.writeFileSync(__dirname + '/input/.babelrc', JSON.stringify(babelrc));

    await nextBundle(b);
    file = fs.readFileSync(__dirname + '/dist/index.js', 'utf8');
    assert(!file.includes('class Foo {}'));
    assert(!file.includes('class Bar {}'));
  });
});


it('should minify in production mode', async function() {

keep keep keep replace replace keep keep keep keep

assert(fs.existsSync(__dirname + '/dist/' + output.getRaw()));


const Asset = require('../Asset');
const urlJoin = require('../utils/urlJoin');
const md5 = require('../utils/md5');

class RawAsset extends Asset {
  // Don't load raw assets. They will be copied by the RawPackager directly.
  load() {}

  generate() {
    // Don't return a URL to the JS bundle if there is a bundle loader defined for this asset type.
    // This will cause the actual asset to be automatically preloaded prior to the JS bundle running.
    if (this.options.bundleLoaders[this.type]) {
      return {};
    }

    const pathToAsset = urlJoin(
      this.options.publicURL,
      this.generateBundleName()
    );

    return {
      js: `module.exports=${JSON.stringify(pathToAsset)};`
    };
  }

  async generateHash() {
    return await md5.file(this.name);
  }
}

module.exports = RawAsset;


if ((!result.length) || result.sorted) {

// sort in place, and record so that we don't sort redundantly

// (we'll sort again if more versions are pushed onto the array)

var _ = require('underscore');
var sqlite3 = require('sqlite3');

var files = require('../../fs/files.js');
var utils = require('../../utils/utils.js');
var buildmessage = require('../../utils/buildmessage.js');
var config = require('../../meteor-services/config.js');
var archinfo = require('../../utils/archinfo.js');
var Console = require('../../console/console.js').Console;

var tropohouse = require('../tropohouse.js');
var packageClient = require('../package-client.js');
var VersionParser = require('../package-version-parser.js');

// XXX: Rationalize these flags.  Maybe use the logger?
var DEBUG_SQL = !!process.env.METEOR_DEBUG_SQL;

var SYNCTOKEN_ID = "1";

var METADATA_LAST_SYNC = "lastsync";

var BUSY_RETRY_ATTEMPTS = 10;
var BUSY_RETRY_INTERVAL = 1000;

var Mutex = function () {
  var self = this;

  self._locked = false;

  self._resolvers = [];
};

_.extend(Mutex.prototype, {
  lock: function () {
    var self = this;

    while (true) {
      if (!self._locked) {
        self._locked = true;
        return;
      }

      new Promise(function (resolve) {
        self._resolvers.push(resolve);
      }).await();
    }
  },

  unlock: function () {
    var self = this;

    if (!self._locked) {
      throw new Error("unlock called on unlocked mutex");
    }

    self._locked = false;
    var resolve = self._resolvers.shift();
    if (resolve) {
      resolve();
    }
  }
});

var Txn = function (db) {
  var self = this;
  self.db = db;
  self.closed = false;
  self.committed = false;
  self.started = false;
};

_.extend(Txn.prototype, {
  // Runs a SQL query and returns the rows
  query: function (sql, params) {
    var self = this;
    return self.db._query(sql, params);
  },

  // Runs a SQL statement, returning no rows
  execute: function (sql, params) {
    var self = this;
    return self.db._execute(sql, params);
  },

  // Start a transaction
  begin: function (mode) {
    var self = this;

    // XXX: Use DEFERRED mode?
    mode = mode || "IMMEDIATE";

    if (self.started) {
      throw new Error("Transaction already started");
    }

    self.db._execute("BEGIN " + mode + " TRANSACTION");
    self.started = true;
  },

  // Releases resources from the transaction; Rollback if commit not already called.
  close: function () {
    var self = this;

    if (self.closed) {
      return;
    }

    if (!self.started) {
      return;
    }

    self.db._execute("ROLLBACK TRANSACTION");
    self.committed = false;
    self.closed = true;
  },

  // Commits the transaction.  close() will then be a no-op
  commit: function () {
    var self = this;

    self.db._execute("END TRANSACTION");
    self.committed = true;
    self.closed = true;
  }
});

var Db = function (dbFile, options) {
  var self = this;

  self._dbFile = dbFile;

  self._autoPrepare = true;
  self._prepared = {};

  self._transactionMutex = new Mutex();

  self._db = self._retry(function () {
    return self.open(dbFile);
  });

  // WAL mode copes much better with (multi-process) concurrency
  self._retry(function () {
    self._execute('PRAGMA journal_mode=WAL');
  });
};

_.extend(Db.prototype, {

  // TODO: Move to utils?
  _retry: function (f, options) {
    options = _.extend({ maxAttempts: 3, delay: 500}, options || {});

    for (var attempt = 1; attempt <= options.maxAttempts; attempt++) {
      try {
        return f();
      } catch (err) {
        if (attempt < options.maxAttempts) {
          Console.warn("Retrying after error", err);
        } else {
          throw err;
        }
      }

      if (options.delay) {
        utils.sleepMs(options.delay);
      }
    }
  },

  // Runs functions serially, in a mutex
  _serialize: function (f) {
    var self = this;

    try {
      self._transactionMutex.lock();
      return f();
    } finally {
      self._transactionMutex.unlock();
    }
  },

  // Do not call any other methods on this object after calling this one.
  // This yields.
  closePermanently: function () {
    var self = this;
    self._closePreparedStatements();
    var db = self._db;
    self._db = null;
    Promise.denodeify(db.close).call(db).await();
  },

  // Runs the function inside a transaction block
  runInTransaction: function (action) {
    var self = this;

    var runOnce = function () {
      var txn = new Txn(self);

      var t1 = Date.now();

      var rollback = true;
      var result = null;
      var resultError = null;

      txn.begin();
      try {
        result = action(txn);
        txn.commit();
      } catch (err) {
        resultError = err;
      } finally {
        try {
          txn.close();
        } catch (e) {
          // We don't have a lot of options here...
          Console.warn("Error closing transaction", e);
        }
      }

      //self._closePreparedStatements();

      if (DEBUG_SQL) {
        var t2 = Date.now();
        // XXX: Hack around not having loggers
        Console.info("Transaction took: ", (t2 - t1));
      }

      if (resultError) {
        throw resultError;
      }

      return result;
    };

    for (var attempt = 0; ; attempt++) {
      try {
        return self._serialize(runOnce);
      } catch (err) {
        var retry = false;
        // Grr... doesn't expose error code; must string-match
        if (err.message
            && (   err.message == "SQLITE_BUSY: database is locked"
                || err.message == "SQLITE_BUSY: cannot commit transaction - SQL statements in progress")) {
          if (attempt < BUSY_RETRY_ATTEMPTS) {
            retry = true;
          }
        }
        if (!retry) {
          throw err;
        }
      }

      // Wait on average BUSY_RETRY_INTERVAL, but randomize to avoid thundering herd
      var t = (Math.random() + 0.5) * BUSY_RETRY_INTERVAL;
      utils.sleepMs(t);
    }
  },

  open: function (dbFile) {
    var self = this;

    if ( !files.exists(files.pathDirname(dbFile)) ) {
      Console.debug("Creating database directory", dbFile);

      var folder = files.pathDirname(dbFile);
      if ( !files.mkdir_p(folder) )
        throw new Error("Could not create folder at " + folder);
    }

    Console.debug("Opening db file", dbFile);
    return new sqlite3.Database(files.convertToOSPath(dbFile));
  },

  // Runs a query synchronously, returning all rows
  // Hidden to enforce transaction usage
  _query: function (sql, params) {
    var self = this;

    var prepared = null;
    var prepare = self._autoPrepare && !_.isEmpty(params);
    if (prepare) {
      prepared = self._prepareWithCache(sql);
    }

    if (DEBUG_SQL) {
      var t1 = Date.now();
    }

    //Console.debug("Executing SQL ", sql, params);

    var promise = prepared
      ? Promise.denodeify(prepared.all).call(prepared, params)
      : Promise.denodeify(self._db.all).call(self._db, sql, params);

    var rows = promise.await();

    if (DEBUG_SQL) {
      var t2 = Date.now();
      if ((t2 - t1) > 10) {
        // XXX: Hack around not having log levels
        Console.info("SQL statement ", sql, " took ", (t2 - t1));
      }
    }

    return rows;
  },

  // Runs a query synchronously, returning no rows
  // Hidden to enforce transaction usage
  _execute: function (sql, params) {
    var self = this;

    var prepared = null;
    // We don't prepare non-parametrized statements, because (a) there's not
    // that much of a win from doing so, since we don't tend to run them in bulk
    // and (b) doing so can trigger
    // https://github.com/mapbox/node-sqlite3/pull/355 .  (We can avoid that bug
    // by being careful to pass in an empty array or no argument for params to
    // prepared.run instead of undefined, but we can also just avoid the issue
    // entirely.)
    var prepare = self._autoPrepare && !_.isEmpty(params);
    if (prepare) {
      prepared = self._prepareWithCache(sql);
    }

    if (DEBUG_SQL) {
      var t1 = Date.now();
    }

    //Console.debug("Executing SQL ", sql, params);

    var ret = new Promise(function (resolve, reject) {
      function callback(err) {
        err ? reject(err) : resolve({
          // Yes, lastID & changes are on this(!)
          lastID: this.lastID,
          changes: this.changes
        });
      }

      if (prepared) {
        prepared.run(params, callback);
      } else {
        self._db.run(sql, params, callback);
      }
    }).await();

    if (DEBUG_SQL) {
      var t2 = Date.now();
      if ((t2 - t1) > 10) {
        Console.info("SQL statement ", sql, " took ", (t2 - t1));
      }
    }

    return ret;
  },

  // Prepares the statement, caching the result
  _prepareWithCache: function (sql) {
    var self = this;

    var prepared = self._prepared[sql];
    if (!prepared) {
      //Console.debug("Preparing statement: ", sql);
      new Promise(function (resolve, reject) {
        prepared = self._db.prepare(sql, function (err) {
          err ? reject(err) : resolve();
        });
      }).await();

      self._prepared[sql] = prepared;
    }

    return prepared;
  },

  // Close any cached prepared statements
  _closePreparedStatements: function () {
    var self = this;

    var prepared = self._prepared;
    self._prepared = {};

    _.each(prepared, function (statement) {
      var err = new Promise(function (resolve) {
        // We resolve the promise with an error instead of rejecting it,
        // because we don't want to throw.
        statement.finalize(resolve);
      }).await();

      if (err) {
        Console.warn("Error finalizing statement ", err);
      }
    });
  }

});

var Table = function (name, jsonFields, options) {
  var self = this;
  options = options || {};

  self.name = name;
  self.jsonFields = jsonFields;
  self.noContentColumn = options.noContentColumn;

  self._buildStatements();
};

_.extend(Table.prototype, {
  _buildStatements: function () {
    var self = this;

    var queryParams = self._generateQuestionMarks(
      self.jsonFields.length + (self.noContentColumn ? 0 : 1));
    self._selectQuery = "SELECT * FROM " + self.name + " WHERE _id=?";
    self._insertQuery = "INSERT INTO " + self.name + " VALUES " + queryParams;
    self._deleteQuery = "DELETE FROM " + self.name + " WHERE _id=?";
  },

  // Generate a string of the form (?, ?) where the n is the number of question
  // mark.
  _generateQuestionMarks: function (n) {
    return "(" + _.times(n, function () { return "?" }).join(",") + ")";
  },

  find: function (txn, id) {
    var self = this;
    var rows = txn.query(self._selectQuery, [ id ]);
    if (rows.length !== 0) {
      if (rows.length !== 1) {
        throw new Error("Corrupt database (PK violation)");
      }
      return rows[0];
    }
    return undefined;
  },

  upsert: function (txn, objects) {
    var self = this;

    // XXX: Use sqlite upsert
    // XXX: Speculative insert
    // XXX: Fix transaction logic so we always roll back
    _.each(objects, function (o) {
      var id = o._id;
      var rows = txn.query(self._selectQuery, [ id ]);
      if (rows.length !== 0) {
        var deleteResults = txn.execute(self._deleteQuery, [ id ]);
        if (deleteResults.changes !== 1) {
          throw new Error("Unable to delete row: " + id);
        }
      }
      var row = [];
      _.each(self.jsonFields, function (jsonField) {
        row.push(o[jsonField]);
      });
      if (! self.noContentColumn) {
        row.push(JSON.stringify(o));
      }
      txn.execute(self._insertQuery, row);
    });
  },

  createTable: function (txn) {
    var self = this;

    var sql = 'CREATE TABLE IF NOT EXISTS ' + self.name + '(';
    for (var i = 0; i < self.jsonFields.length; i++) {
      var jsonField = self.jsonFields[i];
      var sqlColumn = jsonField;
      if (i != 0) sql += ", ";
      sql += sqlColumn + " STRING";
      if (sqlColumn === '_id') {
        sql += " PRIMARY KEY";
      }
    }
    if (! self.noContentColumn) {
      sql += ", content STRING";
    }
    sql += ")";
    txn.execute(sql);

    //sql = "CREATE INDEX IF NOT EXISTS idx_" + self.name + "_id ON " + self.name + "(_id)";
    //txn.execute(sql);
  }
});

// A RemoteCatalog is a local cache of the content of troposphere.
// A default instance of this catalog is registered by the layered catalog and is available
// under the variable "official" from the catalog.js
//
// The remote catalog is backed by a db to make things easier on the memory and for faster queries
var RemoteCatalog = function () {
  var self = this;

  // Set this to true if we are not going to connect to the remote package
  // server, and will only use the cached data for our package information
  // This means that the catalog might be out of date on the latest developments.
  self.offline = null;

  self.db = null;
};

_.extend(RemoteCatalog.prototype, {
  toString: function () {
    var self = this;
    return "RemoteCatalog";
  },

  // Used for special cases that want to ensure that all connections to the DB
  // are closed (eg to ensure that all writes have been flushed from the '-wal'
  // file to the main DB file). Most methods on this class will stop working
  // after you call this method. Note that this yields.
  closePermanently: function () {
    var self = this;
    self.db.closePermanently();
    self.db = null;
  },

  getVersion: function (name, version) {
    var result = this._contentQuery(
      "SELECT content FROM versions WHERE packageName=? AND version=?",
      [name, version]);
    if(!result || result.length === 0) {
      return null;
    }
    return result[0];
  },

  // As getVersion, but returns info on the latest version of the
  // package, or null if the package doesn't exist or has no versions.
  getLatestVersion: function (name) {
    var self = this;

    var versions = self.getSortedVersions(name);
    return self.getVersion(name, _.last(versions));
  },

  getSortedVersions: function (name) {
    var self = this;
    var match = this._columnsQuery(
      "SELECT version FROM versions WHERE packageName=?", name);
    if (match === null)
      return [];
    return _.pluck(match, 'version').sort(VersionParser.compare);
  },

  // Just getVersion mapped over getSortedVersions, but only makes one round
  // trip to sqlite.
  getSortedVersionRecords: function (name) {
    var self = this;
    var versionRecords = this._contentQuery(
      "SELECT content FROM versions WHERE packageName=?", [name]);
    if (! versionRecords)
      return [];
    versionRecords.sort(function (a, b) {
      return VersionParser.compare(a.version, b.version);
    });
    return versionRecords;
  },

  getLatestMainlineVersion: function (name) {
    var self = this;
    var versions = self.getSortedVersions(name);
    versions.reverse();
    var latest = _.find(versions, function (version) {
      return !/-/.test(version);
    });
    if (!latest)
      return null;
    return self.getVersion(name, latest);
  },

  getPackage: function (name, options) {
    var result = this._contentQuery(
      "SELECT content FROM packages WHERE name=?", name);
    if (!result || result.length === 0)
      return null;
    if (result.length !== 1) {
      throw new Error("Found multiple packages matching name: " + name);
    }
    return result[0];
  },

  getAllBuilds: function (name, version) {
    var result = this._contentQuery(
      "SELECT content FROM builds WHERE builds.versionId = " +
        "(SELECT _id FROM versions WHERE versions.packageName=? AND " +
        "versions.version=?)",
      [name, version]);
    if (!result || result.length === 0)
      return null;
    return result;
  },

  // If this package has any builds at this version, return an array of builds
  // which cover all of the required arches, or null if it is impossible to
  // cover them all (or if the version does not exist).
  // Note that this method is specific to RemoteCatalog.
  getBuildsForArches: function (name, version, arches) {
    var self = this;

    var solution = null;
    var allBuilds = self.getAllBuilds(name, version) || [];

    utils.generateSubsetsOfIncreasingSize(allBuilds, function (buildSubset) {
      // This build subset works if for all the arches we need, at least one
      // build in the subset satisfies it. It is guaranteed to be minimal,
      // because we look at subsets in increasing order of size.
      var satisfied = _.all(arches, function (neededArch) {
        return _.any(buildSubset, function (build) {
          var buildArches = build.buildArchitectures.split('+');
          return !!archinfo.mostSpecificMatch(neededArch, buildArches);
        });
      });
      if (satisfied) {
        solution = buildSubset;
        return true;  // stop the iteration
      }
    });
    return solution;  // might be null!
  },

  // Returns general (non-version-specific) information about a
  // release track, or null if there is no such release track.
  getReleaseTrack: function (name) {
    var self = this;
    var result = self._contentQuery(
      "SELECT content FROM releaseTracks WHERE name=?", name);
    if (!result || result.length === 0)
      return null;
    return result[0];
  },

  getReleaseVersion: function (track, version) {
    var self = this;
    var result = self._contentQuery(
      "SELECT content FROM releaseVersions WHERE track=? AND version=?",
      [track, version]);
    if (!result || result.length === 0)
      return null;
    return result[0];
  },

  // Used by make-bootstrap-tarballs. Only should be used on catalogs that are
  // specially constructed for bootstrap tarballs.
  forceRecommendRelease: function (track, version) {
    var self = this;
    var releaseVersionData = self.getReleaseVersion(track, version);
    if (!releaseVersionData) {
      throw Error("Can't force-recommend unknown release " + track + "@"
                  + version);
    }
    releaseVersionData.recommended = true;
    self._insertReleaseVersions([releaseVersionData]);
  },

  getAllReleaseTracks: function () {
    return _.pluck(this._columnsQuery("SELECT name FROM releaseTracks"),
                   'name');
  },

  getAllPackageNames: function () {
    return _.pluck(this._columnsQuery("SELECT name FROM packages"), 'name');
  },

  initialize: function (options) {
    var self = this;

    options = options || {};
    // We should to figure out if we are intending to connect to the package server.
    self.offline = options.offline ? options.offline : false;

    var dbFile = options.packageStorage || config.getPackageStorage();
    self.db = new Db(dbFile);

    self.tableVersions = new Table('versions', ['packageName', 'version', '_id']);
    self.tableBuilds = new Table('builds', ['versionId', '_id']);
    self.tableReleaseTracks = new Table('releaseTracks', ['name', '_id']);
    self.tableReleaseVersions = new Table('releaseVersions', ['track', 'version', '_id']);
    self.tablePackages = new Table('packages', ['name', '_id']);
    self.tableSyncToken = new Table('syncToken', ['_id']);
    self.tableMetadata = new Table('metadata', ['_id']);
    self.tableBannersShown = new Table(
      'bannersShown', ['_id', 'lastShown'], { noContentColumn: true });

    self.allTables = [
      self.tableVersions,
      self.tableBuilds,
      self.tableReleaseTracks,
      self.tableReleaseVersions,
      self.tablePackages,
      self.tableSyncToken,
      self.tableMetadata,
      self.tableBannersShown
    ];
    return self.db.runInTransaction(function(txn) {
      _.each(self.allTables, function (table) {
        table.createTable(txn);
      });

      // Extra indexes for the most expensive queries
      // These are non-unique indexes
      // XXX We used to have a versionsNamesIdx here on versions(packageName);
      //     we no longer create it but we don't waste time dropping it either.
      txn.execute("CREATE INDEX IF NOT EXISTS versionsIdx ON " +
                  "versions(packageName, version)");
      txn.execute("CREATE INDEX IF NOT EXISTS buildsVersionsIdx ON " +
                  "builds(versionId)");
      txn.execute("CREATE INDEX IF NOT EXISTS packagesIdx ON " +
                  "packages(name)");
      txn.execute("CREATE INDEX IF NOT EXISTS releaseTracksIdx ON " +
                  "releaseTracks(name)");
      txn.execute("CREATE INDEX IF NOT EXISTS releaseVersionsIdx ON " +
                  "releaseVersions(track, version)");
    });
  },

  // This function empties the DB. This is called from the package-client.
  reset: function () {
    var self = this;
    return self.db.runInTransaction(function (txn) {
      _.each(self.allTables, function (table) {
        txn.execute("DELETE FROM " + table.name);
      });
    });
  },

  refresh: function (options) {
    var self = this;
    options = options || {};

    Console.debug("In remote catalog refresh");

    if (process.env.METEOR_TEST_FAIL_RELEASE_DOWNLOAD === 'offline') {
      var e = new Error;
      e.errorType = 'DDP.ConnectionError';
      throw e;
    }

    if (self.offline)
      return false;

    if (options.maxAge) {
      var lastSync = self.getMetadata(METADATA_LAST_SYNC);
      Console.debug("lastSync = ", lastSync);
      if (lastSync && lastSync.timestamp) {
        if ((Date.now() - lastSync.timestamp) < options.maxAge) {
          Console.debug("Package catalog is sufficiently up-to-date; not updating\n");
          return false;
        }
      }
    }

    var updateResult = {};
    // XXX This buildmessage.enterJob only exists for showing progress.
    buildmessage.enterJob({ title: 'updating package catalog' }, function () {
      updateResult = packageClient.updateServerPackageData(self);
    });

    if (updateResult.resetData) {
      tropohouse.default.wipeAllPackages();
    }

    return true;
  },

  // Given a release track, returns all recommended versions for this track,
  // sorted by their orderKey. Returns the empty array if the release track does
  // not exist or does not have any recommended versions.
  getSortedRecommendedReleaseVersions: function (track, laterThanOrderKey) {
    var self = this;
    var versions =
          self.getSortedRecommendedReleaseRecords(track, laterThanOrderKey);
    return _.pluck(versions, "version");
  },

  // Given a release track, returns all recommended version *records* for this
  // track, sorted by their orderKey. Returns the empty array if the release
  // track does not exist or does not have any recommended versions.
  getSortedRecommendedReleaseRecords: function (track, laterThanOrderKey) {
    var self = this;
    // XXX releaseVersions content objects are kinda big; if we put
    // 'recommended' and 'orderKey' in their own columns this could be faster
    var result = self._contentQuery(
      "SELECT content FROM releaseVersions WHERE track=?", track);

    var recommended = _.filter(result, function (v) {
      if (!v.recommended)
        return false;
      return !laterThanOrderKey || v.orderKey > laterThanOrderKey;
    });

    var recSort = _.sortBy(recommended, function (rec) {
      return rec.orderKey;
    });
    recSort.reverse();
    return recSort;
  },

  // Given a release track, returns all version records for this track.
  getReleaseVersionRecords: function (track) {
    var self = this;
    var result = self._contentQuery(
      "SELECT content FROM releaseVersions WHERE track=?", track);
    return result;
  },

  // For a given track, returns the total number of release versions on that
  // track.
  getNumReleaseVersions: function (track) {
    var self = this;
    var result = self._columnsQuery(
      "SELECT count(*) FROM releaseVersions WHERE track=?", track);
    return result[0]["count(*)"];
  },

  // Returns the default release version on the DEFAULT_TRACK, or for a
  // given release track.
  getDefaultReleaseVersion: function (track) {
    var self = this;
    var versionRecord = self.getDefaultReleaseVersionRecord(track);
    if (! versionRecord)
      throw new Error("Can't get default release version for track " + track);
    return _.pick(versionRecord, ["track", "version" ]);
  },

  // Returns the default release version record for the DEFAULT_TRACK, or for a
  // given release track.
  getDefaultReleaseVersionRecord: function (track) {
    var self = this;

    if (!track)
      track = exports.DEFAULT_TRACK;

    var versions = self.getSortedRecommendedReleaseRecords(track);
    if (!versions.length)
      return null;
    return  versions[0];
  },

  getBuildWithPreciseBuildArchitectures: function (versionRecord, buildArchitectures) {
    var self = this;
    var matchingBuilds = this._contentQuery(
      "SELECT content FROM builds WHERE versionId=?", versionRecord._id);
    return _.findWhere(matchingBuilds, { buildArchitectures: buildArchitectures });
  },

  // Executes a query, returning an array of each content column parsed as JSON
  _contentQuery: function (query, params) {
    var self = this;
    var rows = self._columnsQuery(query, params);
    return _.map(rows, function(entity) {
      return JSON.parse(entity.content);
    });
  },

  // Executes a query, returning an array of maps from column name to data.
  // No JSON parsing is performed.
  _columnsQuery: function (query, params) {
    var self = this;
    var rows = self.db.runInTransaction(function (txn) {
      return txn.query(query, params);
    });
    return rows;
  },

  _insertReleaseVersions: function(releaseVersions) {
    var self = this;
    return self.db.runInTransaction(function (txn) {
      self.tableReleaseVersions.upsert(txn, releaseVersions);
    });
  },

  //Given data from troposphere, add it into the local store
  insertData: function(serverData, syncComplete) {
    var self = this;
    return self.db.runInTransaction(function (txn) {
      self.tablePackages.upsert(txn, serverData.collections.packages);
      self.tableBuilds.upsert(txn, serverData.collections.builds);
      self.tableVersions.upsert(txn, serverData.collections.versions);
      self.tableReleaseTracks.upsert(txn, serverData.collections.releaseTracks);
      self.tableReleaseVersions.upsert(txn, serverData.collections.releaseVersions);

      var syncToken = serverData.syncToken;
      Console.debug("Adding syncToken: ", JSON.stringify(syncToken));
      syncToken._id = SYNCTOKEN_ID; //Add fake _id so it fits the pattern
      self.tableSyncToken.upsert(txn, [syncToken]);

      if (syncComplete) {
        var lastSync = {timestamp: Date.now()};
        self._setMetadata(txn, METADATA_LAST_SYNC, lastSync);
      }
    });
  },

  getSyncToken: function() {
    var self = this;
    var result = self._contentQuery("SELECT content FROM syncToken WHERE _id=?",
                                    [ SYNCTOKEN_ID ]);
    if (!result || result.length === 0) {
      Console.debug("No sync token found");
      return null;
    }
    if (result.length !== 1) {
      throw new Error("Unexpected number of sync tokens found");
    }
    delete result[0]._id;
    Console.debug("Returning sync token: " + JSON.stringify(result[0]));
    return result[0];
  },

  getMetadata: function(key) {
    var self = this;
    var row = self.db.runInTransaction(function (txn) {
      return self.tableMetadata.find(txn, key);
    });
    if (row) {
      return JSON.parse(row['content']);
    }
    return undefined;
  },

  setMetadata: function(key, value) {
    var self = this;
    self.db.runInTransaction(function (txn) {
      self._setMetadata(txn, key, value);
    });
  },

  _setMetadata: function(txn, key, value) {
    var self = this;
    value._id = key;
    self.tableMetadata.upsert(txn, [value]);
  },

  shouldShowBanner: function (releaseName, bannerDate) {
    var self = this;
    var row = self.db.runInTransaction(function (txn) {
      return self.tableBannersShown.find(txn, releaseName);
    });
    // We've never printed a banner for this release.
    if (! row)
      return true;
    try {
      var lastShown = new Date(JSON.parse(row.lastShown));
      return lastShown < bannerDate;
    } catch (e) {
      // Probably an error in JSON.parse or something. Just show the banner.
      return true;
    }
  },

  setBannerShownDate: function (releaseName, bannerShownDate) {
    var self = this;
    self.db.runInTransaction(function (txn) {
      self.tableBannersShown.upsert(txn, [{
        _id: releaseName,
        // XXX For now, there's no way to tell this file to make a non-string
        // column in a sqlite table, but this should probably change to a
        // 'timestamp with time zone' or whatever.
        lastShown: JSON.stringify(bannerShownDate)
      }]);
    });
  }
});

exports.RemoteCatalog = RemoteCatalog;

// We put this constant here because we don't have any better place that would otherwise cause a cycle
exports.DEFAULT_TRACK = 'METEOR';

// The catalog as provided by troposhere (aka atomospherejs.com)
exports.official = new RemoteCatalog();