<commit_before>"use strict"

const argv = new Set(process.argv.slice(2))
const NODE_ENV = [...argv].filter(v => v.indexOf("--NODE_ENV=") == 0)
                          .map(v => v.split("=")[1])[0] || null

const { exec, fork, spawn, spawnSync } = require("child_process")
const { createReadStream, createWriteStream, mkdir, watch } = require("fs")
const { dirname, resolve:resolvePath, join:joinPath } = require("path")
const { createInterface:createReadlineInterface } = require("readline")
const { inherits } = require("util")
const { nextTick } = process
const { typeOf } = require("ippankiban/lib/type")
const { networkInterfaces } = require("os")

const { class:klass } = require("ippankiban/lib/class")
const { Node } = require("ippankiban/lib/Node")
const { UID:{uid} } = require("ippankiban/lib/UID")
const { UnixSocketServer } = require("ippanserver/lib/UnixSocketServer")

const cwd = resolvePath(process.cwd(), dirname(process.mainModule.filename))

module.exports.App = klass(Node, statics => {
    const apps = new WeakMap
    const TMP = "/tmp" //TODO find alternatives on alternative OS

    const nl_comment_at_0 = char => char == "#" || char == ";"

    return {
        constructor: function(){
            Node.call(this)
            apps.set(this, new Map)

            Promise.all([
                new Promise(resolve => {
                  const mandatories = new Set()

                  const input = createReadStream( resolvePath(cwd, "./.flags") )
                  const lines = createReadlineInterface({ input })

                  const online = line => {
                      line = line.trim()
                      if ( nl_comment_at_0(line[0]) || !line.length )
                        return

                      const parts = line.split(/\s/)
                                        .filter(v => !!v)

                      parts.forEach(part => mandatories.add(part))
                  }

                  const onend = () => {
                      input.removeListener("error", onerror)
                      lines.removeListener("error", onerror)
                      lines.removeListener("line", online)
                      lines.removeListener("close", onend)
                      resolve(mandatories)
                  }

                  const onerror = e => {
                      if ( e && e.code == "ENOENT" )
                        onend()
                      else {
                        throw e
                      }
                  }

                  input.addListener("error", onerror)
                  lines.addListener("error", onerror)
                  lines.addListener("line", online)
                  lines.addListener("close", onend)
                }).then(mandatories => new Promise(resolve => {
                      const execArgv = new Set(process.execArgv)
                      let ok = true

                      mandatories.forEach(arg => {
                          if ( execArgv.has(arg) )
                            return
                          ok = false
                          execArgv.add(arg)
                      })

                      if ( ok )
                        return resolve()

                      const cp = fork(resolvePath(cwd, process.mainModule.filename), process.argv.slice(2), {execArgv: [...execArgv]})

                      process.addListener("SIGINT", () => cp.kill())
                      process.addListener("SIGTERM", () => cp.kill())
                      process.addListener("exit", () => cp.kill())
                      cp.addListener("SIGINT", () => process.exit())
                      cp.addListener("SIGTERM", () => process.exit())
                      cp.addListener("exit", () => process.exit())

                      const e = new Error("missing mandatory parameters, restarting with child process")
                      e.warn = true
                      throw e
                  }))
            ])
            .then(() => new Promise(resolve => {
                //process.setMaxListeners(10)
                process.on("SIGINT", e => process.exit())
                process.on("SIGTERM", e => process.exit())

                resolve()
            }))
            .then(() => new Promise(resolve => {
                /*
                    project environment configuration
                */
                const input = createReadStream( resolvePath(cwd, "./.env") )
                const lines = createReadlineInterface({ input })
                const variables = new Map
                const pairs = new Map

                const online = line => {
                    line = line.trim()
                    if ( nl_comment_at_0(line[0]) || !line.length )
                      return
                    else if ( line[0] == "$" ) {
                        const idx = line.indexOf("=")
                        variables.set(line.slice(1, idx).trim(), line.slice(idx+1).trim())
                    }
                    else {
                        const idx = line.indexOf("=")
                        const key = line.slice(0, idx).trim()
                        let value = line.slice(idx+1).trim()

                        if ( value[0] == "[" && value[value.length-1] == "]")
                            value = value.slice(1, -1)
                                         .split(",")
                                         .map(v => v.trim())

                        pairs.set(key, value)
                    }
                }

                const onend = () => {
                    input.removeListener("error", onerror)
                    lines.removeListener("error", onerror)
                    lines.removeListener("line", online)
                    lines.removeListener("close", onend)

                    pairs.forEach((value, key) => {
                        variables.forEach((replace, match) => {
                            if ( typeOf(value) == "string" )
                              value = value.replace(new RegExp(`\\$${match}`, "g"), replace)
                            else if ( typeOf(value) == "array" )
                              value = value.map(v => v.replace(new RegExp(`\\$${match}`, "g"), replace))
                        })

                        process.env[key.toUpperCase()] = value
                    })

                    variables.clear()
                    pairs.clear()
                    resolve()
                }

                const onerror = e => {
                    if ( e && e.code == "ENOENT" )
                      onend()
                    else {
                      throw e
                    }
                }

                input.addListener("error", onerror)
                lines.addListener("error", onerror)
                lines.addListener("line", online)
                lines.addListener("close", onend)
            }))
            .then(() => new Promise(resolve => {
                /*
                    user config, can be present or not
                */
                const input = createReadStream( resolvePath(cwd, "./.local") )
                const lines = createReadlineInterface({ input })
                const variables = new Map
                const pairs = new Map

                const online = line => {
                    line = line.trim()
                    if ( nl_comment_at_0(line[0]) || !line.length )
                      return
                    else if ( line[0] == "$" ) {
                        const idx = line.indexOf("=")
                        variables.set(line.slice(1, idx).trim(), line.slice(idx+1).trim())
                    }
                    else {
                        const idx = line.indexOf("=")
                        const key = line.slice(0, idx).trim()
                        let value = line.slice(idx+1).trim()

                        if ( value[0] == "[" && value[value.length-1] == "]")
                            value = value.slice(1, -1)
                                         .split(",")
                                         .map(v => v.trim())

                        pairs.set(key, value)
                    }
                }

                const onend = () => {
                    input.removeListener("error", onerror)
                    lines.removeListener("error", onerror)
                    lines.removeListener("line", online)
                    lines.removeListener("close", onend)

                    pairs.forEach((value, key) => {
                        variables.forEach((replace, match) => {
                            if ( typeOf(value) == "string" )
                              value = value.replace(new RegExp(`\\$${match}`, "g"), replace)
                            else if ( typeOf(value) == "array" )
                              value = value.map(v => v.replace(new RegExp(`\\$${match}`, "g"), replace))
                        })

                        process.env[key.toUpperCase()] = value
                    })

                    variables.clear()
                    pairs.clear()

                    resolve()
                }

                const onerror = e => {
                    if ( e && e.code == "ENOENT" )
                      onend()
                    else {
                      throw e
                    }
                }

                input.addListener("error", onerror)
                lines.addListener("error", onerror)
                lines.addListener("line", online)
                lines.addListener("close", onend)
            }))
            .then(() => new Promise(resolve => {
                apps.get(this).set("uid", uid())

                resolve()
            }))
            .then(() => new Promise(resolve => {
                const mode = 0O0775 & (~process.umask())
                const ifaces = networkInterfaces()

                process.env.NODE_ENV = process.env.NODE_ENV  || NODE_ENV || "dev"
                process.env.ROOT = cwd
                process.env.TMP = this.tmp_path
                process.env.APP_SOCKET = this.socket_path

                Object.keys(ifaces).forEach(ifname => {
                    ifaces[ifname].forEach(({address, family}) => {
                        if ( family == "IPv4" && address !== "127.0.0.1" )
                          process.env.IPV4 = address
                    })
                })

                mkdir(process.env.TMP, mode, err => {
                    if ( err ) throw err

                    process.addListener("SIGINT", () => process.exit())
                    process.addListener("SIGTERM", () => process.exit())
                    process.addListener("exit", code => {
                        console.log(`[ippanAppManager] attempting to delete ${process.env.TMP}`)
                        spawn("rm", ["-rf", process.env.TMP])
                    })

                    resolve()
                })
            }))
            .then(() => new Promise(resolve => {
                apps.get(this).set("unixSocketServer", new UnixSocketServer(this.socket_path))
                this.appendChild(this.unixSocketServer)

                this.unixSocketServer.addEventListener("listening", e => {
                    console.log(`app socket listening (${this.unixSocketServer.socket})`)
                    this.dispatchEvent("unixsocketserver")
                    resolve()
                })
            }))
            .then(() => new Promise((resolve, reject) => {
                const input = createReadStream( resolvePath(cwd, ".children") )
                const lines = createReadlineInterface({ input })

                let processing = Promise.resolve()
                const online = line => {
                    line = line.trim()
                    if ( nl_comment_at_0(line[0]) || !line.length )
                      return

                    processing = processing.then(() => new Promise(resolve => {
                        const parts = line.split(/\s|\[|\]|,/gi)
                                          .filter(v => !!v)
                                          .map(v => joinPath(cwd ,v.trim()))
                        const main = parts.shift()
                        const to_watch = parts

                        let queue = Promise.resolve()
                        const start_child = prev_cp => new Promise(resolve => {
                            const start = () => {
                                const cp = fork(main, process.argv.slice(2), { env: process.env })
                                console.log(`starting child pid ${cp.pid}`)
                                cp.onexit = code => {
                                    cp.kill()
                                    //exec(`kill -9 ${cp.pid}`)
                                }
                                // process.addListener("SIGINT", cp.onexit)
                                // process.addListener("SIGTERM", cp.onexit)
                                process.addListener("exit", cp.onexit)
                                resolve(cp)
                            }

                            if ( prev_cp && prev_cp.connected ) {
                                const onexit = () => {
                                    console.log(`killing child pid ${prev_cp.pid}`)
                                    prev_cp.removeListener("exit", onexit)
                                    nextTick(() => start())
                                }

                                // process.removeListener("SIGTERM", prev_cp.onexit)
                                // process.removeListener("SIGINT", prev_cp.onexit)
                                process.removeListener("exit", prev_cp.onexit)
                                prev_cp.addListener("exit", onexit)
                                prev_cp.kill()
                                //exec(`kill -9 ${prev_cp.pid}`)
                            } else start()
                        })
                        const onchange = (change, filename) => {
                            if ( filename ) console.log(`[${main}] change detected in ${filename}`)
                            else console.log(`[${main}] change detected`)

                            queue = queue.then(start_child)
                            return queue
                        }

                        to_watch.forEach(path => {
                            const watcher = watch(path)
                            watcher.addListener("change", onchange)
                            watcher.addListener("error", e => console.error(e))
                        })
                        onchange()
                          .then(resolve)
                    }).catch(e => {
                        reject(e)
                    }))
                }

                const onend = () => {
                    lines.removeListener("line", online)
                    lines.removeListener("close", onend)
                    resolve()
                }

                const onerror = e => {
                    if ( e && e.code == "ENOENT" )
                      onend()
                    else {
                      throw e
                    }
                }

                input.addListener("error", onerror)
                lines.addListener("line", online)
                lines.addListener("close", onend)
            }))
            .then(() => new Promise((resolve, reject) => {
                const input = createReadStream( resolvePath(cwd, ".children.local") )
                const lines = createReadlineInterface({ input })

                let processing = Promise.resolve()
                const online = line => {
                    line = line.trim()
                    if ( nl_comment_at_0(line[0]) || !line.length )
                      return

                    processing = processing.then(() => new Promise(resolve => {
                        const parts = line.split(/\s|\[|\]|,/gi)
                                          .filter(v => !!v)
                                          .map(v => joinPath(cwd ,v.trim()))
                        const main = parts.shift()
                        const to_watch = parts

                        let queue = Promise.resolve()
                        const start_child = prev_cp => new Promise(resolve => {
                            const start = () => {
                                const cp = fork(main, process.argv.slice(2), { env: process.env })
                                console.log(`starting child pid ${cp.pid}`)
                                cp.onexit = code => {
                                    cp.kill()
                                    //exec(`kill -9 ${cp.pid}`)
                                }
                                //process.addListener("SIGINT", cp.onexit)
                                //process.addListener("SIGTERM", cp.onexit)
                                process.addListener("exit", cp.onexit)
                                resolve(cp)
                            }

                            if ( prev_cp && prev_cp.connected ) {
                                const onexit = () => {
                                    console.log(`killing child pid ${prev_cp.pid}`)
                                    prev_cp.removeListener("exit", onexit)
                                    nextTick(() => start())
                                }

                                //process.removeListener("SIGINT", prev_cp.onexit)
                                //process.removeListener("SIGTERM", prev_cp.onexit)
                                process.removeListener("exit", prev_cp.onexit)
                                prev_cp.addListener("exit", onexit)
                                prev_cp.kill()
                                //exec(`kill -9 ${prev_cp.pid}`)
                            } else start()
                        })
                        const onchange = (change, filename) => {
                            if ( filename ) console.log(`[${main}] change detected in ${filename}`)
                            else console.log(`[${main}] change detected`)

                            queue = queue.then(start_child)
                            return queue
                        }

                        to_watch.forEach(path => {
                            const watcher = watch(path)
                            watcher.addListener("change", onchange)
                            watcher.addListener("error", e => console.error(e))
                        })
                        onchange()
                          .then(resolve)
                    }).catch(e => {
                        reject(e)
                    }))
                }

                const onend = () => {
                    lines.removeListener("line", online)
                    lines.removeListener("close", onend)
                    resolve()
                }

                const onerror = e => {
                    if ( e && e.code == "ENOENT" )
                      onend()
                    else {
                      throw e
                    }
                }

                input.addListener("error", onerror)
                lines.addListener("line", online)
                lines.addListener("close", onend)
            }))
            .then(() => this.dispatchEvent("ready"))
            .catch(e => {
                if ( e.warn )
                  this.dispatchEvent("warn", e)
                else
                  this.dispatchEvent("error", e)
            })
        }
      , socket_path: { enumerable: true,
            get: function(){
                return resolvePath(this.tmp_path, `./${this.uid}.sock`)
            }
        }
      , tmp_path: { enumerable: true,
            get: function(){
                return resolvePath(TMP, `./${this.uid}`)
            }
        }
      , uid: { enumerable: true,
            get: function(){
                return apps.get(this).get("uid")
            }
        }
      , unixSocketServer: { enumerable: true,
            get: function(){
                return apps.get(this).get("unixSocketServer")
            }
        }
    }
})
<commit_msg><commit_after>"use strict"

const argv = new Set(process.argv.slice(2))
const NODE_ENV = [...argv].filter(v => v.indexOf("--NODE_ENV=") == 0 || v.indexOf("--env=") == 0)
                          .map(v => v.split("=")[1])[0] || null

const { exec, fork, spawn, spawnSync } = require("child_process")
const { createReadStream, createWriteStream, mkdir, watch } = require("fs")
const { dirname, resolve:resolvePath, join:joinPath } = require("path")
const { createInterface:createReadlineInterface } = require("readline")
const { inherits } = require("util")
const { nextTick } = process
const { typeOf } = require("ippankiban/lib/type")
const { networkInterfaces } = require("os")

const { class:klass } = require("ippankiban/lib/class")
const { Node } = require("ippankiban/lib/Node")
const { UID:{uid} } = require("ippankiban/lib/UID")
const { UnixSocketServer } = require("ippanserver/lib/UnixSocketServer")

const cwd = resolvePath(process.cwd(), dirname(process.mainModule.filename))

module.exports.App = klass(Node, statics => {
    const apps = new WeakMap
    const TMP = "/tmp" //TODO find alternatives on alternative OS

    const nl_comment_at_0 = char => char == "#" || char == ";"

    return {
        constructor: function(){
            Node.call(this)
            apps.set(this, new Map)

            Promise.all([
                new Promise(resolve => {
                  const mandatories = new Set()

                  const input = createReadStream( resolvePath(cwd, "./.flags") )
                  const lines = createReadlineInterface({ input })

                  const online = line => {
                      line = line.trim()
                      if ( nl_comment_at_0(line[0]) || !line.length )
                        return

                      const parts = line.split(/\s/)
                                        .filter(v => !!v)

                      parts.forEach(part => mandatories.add(part))
                  }

                  const onend = () => {
                      input.removeListener("error", onerror)
                      lines.removeListener("error", onerror)
                      lines.removeListener("line", online)
                      lines.removeListener("close", onend)
                      resolve(mandatories)
                  }

                  const onerror = e => {
                      if ( e && e.code == "ENOENT" )
                        onend()
                      else {
                        throw e
                      }
                  }

                  input.addListener("error", onerror)
                  lines.addListener("error", onerror)
                  lines.addListener("line", online)
                  lines.addListener("close", onend)
                }).then(mandatories => new Promise(resolve => {
                      const execArgv = new Set(process.execArgv)
                      let ok = true

                      mandatories.forEach(arg => {
                          if ( execArgv.has(arg) )
                            return
                          ok = false
                          execArgv.add(arg)
                      })

                      if ( ok )
                        return resolve()

                      const cp = fork(resolvePath(cwd, process.mainModule.filename), process.argv.slice(2), {execArgv: [...execArgv]})

                      process.addListener("SIGINT", () => cp.kill())
                      process.addListener("SIGTERM", () => cp.kill())
                      process.addListener("exit", () => cp.kill())
                      cp.addListener("SIGINT", () => process.exit())
                      cp.addListener("SIGTERM", () => process.exit())
                      cp.addListener("exit", () => process.exit())

                      const e = new Error("missing mandatory parameters, restarting with child process")
                      e.warn = true
                      throw e
                  }))
            ])
            .then(() => new Promise(resolve => {
                //process.setMaxListeners(10)
                process.on("SIGINT", e => process.exit())
                process.on("SIGTERM", e => process.exit())

                resolve()
            }))
            .then(() => new Promise(resolve => {
                /*
                    project environment configuration
                */
                const input = createReadStream( resolvePath(cwd, "./.env") )
                const lines = createReadlineInterface({ input })
                const variables = new Map
                const pairs = new Map

                const online = line => {
                    line = line.trim()
                    if ( nl_comment_at_0(line[0]) || !line.length )
                      return
                    else if ( line[0] == "$" ) {
                        const idx = line.indexOf("=")
                        variables.set(line.slice(1, idx).trim(), line.slice(idx+1).trim())
                    }
                    else {
                        const idx = line.indexOf("=")
                        const key = line.slice(0, idx).trim()
                        let value = line.slice(idx+1).trim()

                        if ( value[0] == "[" && value[value.length-1] == "]")
                            value = value.slice(1, -1)
                                         .split(",")
                                         .map(v => v.trim())

                        pairs.set(key, value)
                    }
                }

                const onend = () => {
                    input.removeListener("error", onerror)
                    lines.removeListener("error", onerror)
                    lines.removeListener("line", online)
                    lines.removeListener("close", onend)

                    pairs.forEach((value, key) => {
                        variables.forEach((replace, match) => {
                            if ( typeOf(value) == "string" )
                              value = value.replace(new RegExp(`\\$${match}`, "g"), replace)
                            else if ( typeOf(value) == "array" )
                              value = value.map(v => v.replace(new RegExp(`\\$${match}`, "g"), replace))
                        })

                        process.env[key.toUpperCase()] = value
                    })

                    variables.clear()
                    pairs.clear()
                    resolve()
                }

                const onerror = e => {
                    if ( e && e.code == "ENOENT" )
                      onend()
                    else {
                      throw e
                    }
                }

                input.addListener("error", onerror)
                lines.addListener("error", onerror)
                lines.addListener("line", online)
                lines.addListener("close", onend)
            }))
            .then(() => new Promise(resolve => {
                /*
                    user config, can be present or not
                */
                const input = createReadStream( resolvePath(cwd, "./.local") )
                const lines = createReadlineInterface({ input })
                const variables = new Map
                const pairs = new Map

                const online = line => {
                    line = line.trim()
                    if ( nl_comment_at_0(line[0]) || !line.length )
                      return
                    else if ( line[0] == "$" ) {
                        const idx = line.indexOf("=")
                        variables.set(line.slice(1, idx).trim(), line.slice(idx+1).trim())
                    }
                    else {
                        const idx = line.indexOf("=")
                        const key = line.slice(0, idx).trim()
                        let value = line.slice(idx+1).trim()

                        if ( value[0] == "[" && value[value.length-1] == "]")
                            value = value.slice(1, -1)
                                         .split(",")
                                         .map(v => v.trim())

                        pairs.set(key, value)
                    }
                }

                const onend = () => {
                    input.removeListener("error", onerror)
                    lines.removeListener("error", onerror)
                    lines.removeListener("line", online)
                    lines.removeListener("close", onend)

                    pairs.forEach((value, key) => {
                        variables.forEach((replace, match) => {
                            if ( typeOf(value) == "string" )
                              value = value.replace(new RegExp(`\\$${match}`, "g"), replace)
                            else if ( typeOf(value) == "array" )
                              value = value.map(v => v.replace(new RegExp(`\\$${match}`, "g"), replace))
                        })

                        process.env[key.toUpperCase()] = value
                    })

                    variables.clear()
                    pairs.clear()

                    resolve()
                }

                const onerror = e => {
                    if ( e && e.code == "ENOENT" )
                      onend()
                    else {
                      throw e
                    }
                }

                input.addListener("error", onerror)
                lines.addListener("error", onerror)
                lines.addListener("line", online)
                lines.addListener("close", onend)
            }))
            .then(() => new Promise(resolve => {
                apps.get(this).set("uid", uid())

                resolve()
            }))
            .then(() => new Promise(resolve => {
                const mode = 0O0775 & (~process.umask())
                const ifaces = networkInterfaces()

                process.env.NODE_ENV = process.env.NODE_ENV  || NODE_ENV || "dev"
                process.env.ROOT = cwd
                process.env.TMP = this.tmp_path
                process.env.APP_SOCKET = this.socket_path

                Object.keys(ifaces).forEach(ifname => {
                    ifaces[ifname].forEach(({address, family}) => {
                        if ( family == "IPv4" && address !== "127.0.0.1" )
                          process.env.IPV4 = address
                    })
                })

                mkdir(process.env.TMP, mode, err => {
                    if ( err ) throw err

                    process.addListener("SIGINT", () => process.exit())
                    process.addListener("SIGTERM", () => process.exit())
                    process.addListener("exit", code => {
                        console.log(`[ippanAppManager] attempting to delete ${process.env.TMP}`)
                        spawn("rm", ["-rf", process.env.TMP])
                    })

                    resolve()
                })
            }))
            .then(() => new Promise(resolve => {
                apps.get(this).set("unixSocketServer", new UnixSocketServer(this.socket_path))
                this.appendChild(this.unixSocketServer)

                this.unixSocketServer.addEventListener("listening", e => {
                    console.log(`app socket listening (${this.unixSocketServer.socket})`)
                    this.dispatchEvent("unixsocketserver")
                    resolve()
                })
            }))
            .then(() => new Promise((resolve, reject) => {
                const input = createReadStream( resolvePath(cwd, ".children") )
                const lines = createReadlineInterface({ input })

                let processing = Promise.resolve()
                const online = line => {
                    line = line.trim()
                    if ( nl_comment_at_0(line[0]) || !line.length )
                      return

                    processing = processing.then(() => new Promise(resolve => {
                        const parts = line.split(/\s|\[|\]|,/gi)
                                          .filter(v => !!v)
                                          .map(v => joinPath(cwd ,v.trim()))
                        const main = parts.shift()
                        const to_watch = parts

                        let queue = Promise.resolve()
                        const start_child = prev_cp => new Promise(resolve => {
                            const start = () => {
                                const cp = fork(main, process.argv.slice(2), { env: process.env })
                                console.log(`starting child pid ${cp.pid}`)
                                cp.onexit = code => {
                                    cp.kill()
                                    //exec(`kill -9 ${cp.pid}`)
                                }
                                // process.addListener("SIGINT", cp.onexit)
                                // process.addListener("SIGTERM", cp.onexit)
                                process.addListener("exit", cp.onexit)
                                resolve(cp)
                            }

                            if ( prev_cp && prev_cp.connected ) {
                                const onexit = () => {
                                    console.log(`killing child pid ${prev_cp.pid}`)
                                    prev_cp.removeListener("exit", onexit)
                                    nextTick(() => start())
                                }

                                // process.removeListener("SIGTERM", prev_cp.onexit)
                                // process.removeListener("SIGINT", prev_cp.onexit)
                                process.removeListener("exit", prev_cp.onexit)
                                prev_cp.addListener("exit", onexit)
                                prev_cp.kill()
                                //exec(`kill -9 ${prev_cp.pid}`)
                            } else start()
                        })
                        const onchange = (change, filename) => {
                            if ( filename ) console.log(`[${main}] change detected in ${filename}`)
                            else console.log(`[${main}] change detected`)

                            queue = queue.then(start_child)
                            return queue
                        }

                        to_watch.forEach(path => {
                            const watcher = watch(path)
                            watcher.addListener("change", onchange)
                            watcher.addListener("error", e => console.error(e))
                        })
                        onchange()
                          .then(resolve)
                    }).catch(e => {
                        reject(e)
                    }))
                }

                const onend = () => {
                    lines.removeListener("line", online)
                    lines.removeListener("close", onend)
                    resolve()
                }

                const onerror = e => {
                    if ( e && e.code == "ENOENT" )
                      onend()
                    else {
                      throw e
                    }
                }

                input.addListener("error", onerror)
                lines.addListener("line", online)
                lines.addListener("close", onend)
            }))
            .then(() => new Promise((resolve, reject) => {
                const input = createReadStream( resolvePath(cwd, ".children.local") )
                const lines = createReadlineInterface({ input })

                let processing = Promise.resolve()
                const online = line => {
                    line = line.trim()
                    if ( nl_comment_at_0(line[0]) || !line.length )
                      return

                    processing = processing.then(() => new Promise(resolve => {
                        const parts = line.split(/\s|\[|\]|,/gi)
                                          .filter(v => !!v)
                                          .map(v => joinPath(cwd ,v.trim()))
                        const main = parts.shift()
                        const to_watch = parts

                        let queue = Promise.resolve()
                        const start_child = prev_cp => new Promise(resolve => {
                            const start = () => {
                                const cp = fork(main, process.argv.slice(2), { env: process.env })
                                console.log(`starting child pid ${cp.pid}`)
                                cp.onexit = code => {
                                    cp.kill()
                                    //exec(`kill -9 ${cp.pid}`)
                                }
                                //process.addListener("SIGINT", cp.onexit)
                                //process.addListener("SIGTERM", cp.onexit)
                                process.addListener("exit", cp.onexit)
                                resolve(cp)
                            }

                            if ( prev_cp && prev_cp.connected ) {
                                const onexit = () => {
                                    console.log(`killing child pid ${prev_cp.pid}`)
                                    prev_cp.removeListener("exit", onexit)
                                    nextTick(() => start())
                                }

                                //process.removeListener("SIGINT", prev_cp.onexit)
                                //process.removeListener("SIGTERM", prev_cp.onexit)
                                process.removeListener("exit", prev_cp.onexit)
                                prev_cp.addListener("exit", onexit)
                                prev_cp.kill()
                                //exec(`kill -9 ${prev_cp.pid}`)
                            } else start()
                        })
                        const onchange = (change, filename) => {
                            if ( filename ) console.log(`[${main}] change detected in ${filename}`)
                            else console.log(`[${main}] change detected`)

                            queue = queue.then(start_child)
                            return queue
                        }

                        to_watch.forEach(path => {
                            const watcher = watch(path)
                            watcher.addListener("change", onchange)
                            watcher.addListener("error", e => console.error(e))
                        })
                        onchange()
                          .then(resolve)
                    }).catch(e => {
                        reject(e)
                    }))
                }

                const onend = () => {
                    lines.removeListener("line", online)
                    lines.removeListener("close", onend)
                    resolve()
                }

                const onerror = e => {
                    if ( e && e.code == "ENOENT" )
                      onend()
                    else {
                      throw e
                    }
                }

                input.addListener("error", onerror)
                lines.addListener("line", online)
                lines.addListener("close", onend)
            }))
            .then(() => this.dispatchEvent("ready"))
            .catch(e => {
                if ( e.warn )
                  this.dispatchEvent("warn", e)
                else
                  this.dispatchEvent("error", e)
            })
        }
      , socket_path: { enumerable: true,
            get: function(){
                return resolvePath(this.tmp_path, `./${this.uid}.sock`)
            }
        }
      , tmp_path: { enumerable: true,
            get: function(){
                return resolvePath(TMP, `./${this.uid}`)
            }
        }
      , uid: { enumerable: true,
            get: function(){
                return apps.get(this).get("uid")
            }
        }
      , unixSocketServer: { enumerable: true,
            get: function(){
                return apps.get(this).get("unixSocketServer")
            }
        }
    }
})
<|endoftext|>


<commit_before>var util = require('./util.js');
var log = require('./log.js');

var events = {
  messages : {},
  subscribe: function(topic, func, priority){
    var self = this;
    var nPriority = parseInt(priority);
    nPriority = nPriority ? nPriority : 10; // 默认优先级为10
    var sTopicEncode = encodeURIComponent(topic);
    //
    if(typeof self.messages[sTopicEncode] === 'undefined'){
      self.messages[sTopicEncode] = [];
    }
    if(typeof self.messages[sTopicEncode][nPriority] === 'undefined'){
      self.messages[sTopicEncode][nPriority] = [];
    }
    var aPriorityList = self.messages[sTopicEncode][nPriority];
    var nFuncIndex = -1;
    if(aPriorityList){
      if(util.inArray(func, aPriorityList) === - 1){ // 只允许绑定一个相同函数
        aPriorityList.push(func);
        nFuncIndex = aPriorityList.length - 1;
      }
      log({subscribe: topic, priority: nPriority, sort: nFuncIndex});
    }
    return {
      unsubscribe : function(){
        if(nFuncIndex !== -1){
          log({unsubscribe: topic, priority: nPriority, sort: nFuncIndex});
          aPriorityList.splice(nFuncIndex, 1);
          nFuncIndex = -1;
        }
      }
    };
  },
  publish : function(topic, data, done){
    var self = this;
    if(typeof topic !== 'string'){
      return;
    }
    data = data ? data : {};
    var sTopicEncode = encodeURIComponent(topic);
    var aMainList = self.messages[sTopicEncode];
    //
    log({published: topic});
    //
    if(typeof aMainList === 'undefined'){
      return;
    }
    var scope = data._scope || this;
    var bNext = true;
    util.forEach(aMainList, function(aPriorityList, nPriority){
      if(aPriorityList){
        util.forEach(aPriorityList, function(oSub, nFuncIndex){
          if(oSub){
            bNext = oSub.call(scope, data);
            if(bNext !== false){
              bNext = true;
            }
            log({exec: topic, priority: nPriority, sort: nFuncIndex, 'continue': bNext});
            return bNext;
          }
        });
        return bNext;
      }
    });
    if(done){
      done();
    }
  },
};

exports.subscribe = events.subscribe;

exports.publish = events.publish;

exports.messages = events.messages;

/**
 * 发布/订阅事件
 *
 * ```subscribe
 * `` topic
 * &` 类型：String<br/>需要订阅的事件，如'cart.get.done'。
 * `` callback
 * &` 类型：Function( publish参数data )<br/>事件发布后的回调函数，回调函数如果^^^return false^^^,则不继续执行该事件的其他回调函数。
 * `` priority
 * &` 类型：Number ( 默认值：10 )<br/>回调函数执行的优先级，SDK默认的优先级为10，如果要先于默认回调执行，请使用1~9的优先级，如果要先于默认回调执行，请大于10的优先级。
 *
 * 如何取消订阅？
 * ^^^
 * <script>
 *   var s = events.subscribe('hello', function(){
 *     console.log('hello');
 *   }); // 成功订阅后会返回引用
 *   //
 *   s.unsubscribe(); //取消订阅
 * </script>
 * ^^^
 * ```
 *
 * ```publish
 * `` topic
 * &` 类型：String<br/>需要发布的事件，如'cart.get.done'。
 * `` data
 * &` 类型：Object
 * &&` res 类型：Object<br/>请不要使用此保留 key，用于 SDK 从 API 获取数据后传至回调函数。
 * &&` anything 类型：Object 选填<br/>使用除 res 之外 key 的对象，可以向订阅事件传送额外数据。
 * ```
 *
 * @param {subscribe} `topic,callback[,priority]` 订阅事件
 * @param {publish} `topic,data` 发布事件
 *
 */<commit_msg>add the repeated subscribe log<commit_after>var util = require('./util.js');
var log = require('./log.js');

var events = {
  messages : {},
  subscribe: function(topic, func, priority){
    var self = this;
    var nPriority = parseInt(priority);
    nPriority = nPriority ? nPriority : 10; // 默认优先级为10
    var sTopicEncode = encodeURIComponent(topic);
    //
    if(typeof self.messages[sTopicEncode] === 'undefined'){
      self.messages[sTopicEncode] = [];
    }
    if(typeof self.messages[sTopicEncode][nPriority] === 'undefined'){
      self.messages[sTopicEncode][nPriority] = [];
    }
    var aPriorityList = self.messages[sTopicEncode][nPriority];
    var nFuncIndex = -1;
    if(aPriorityList){
      if(util.inArray(func, aPriorityList) === - 1){ // 只允许绑定一个相同函数
        aPriorityList.push(func);
        nFuncIndex = aPriorityList.length - 1;
        log({subscribe: topic, priority: nPriority, sort: nFuncIndex});
      }else{
        log({subscribe: topic, priority: nPriority, sort: '重复监听，忽略'});
      }
    }
    return {
      unsubscribe : function(){
        if(nFuncIndex !== -1){
          log({unsubscribe: topic, priority: nPriority, sort: nFuncIndex});
          aPriorityList.splice(nFuncIndex, 1);
          nFuncIndex = -1;
        }
      }
    };
  },
  publish : function(topic, data, done){
    var self = this;
    if(typeof topic !== 'string'){
      return;
    }
    data = data ? data : {};
    var sTopicEncode = encodeURIComponent(topic);
    var aMainList = self.messages[sTopicEncode];
    //
    log({published: topic});
    //
    if(typeof aMainList === 'undefined'){
      return;
    }
    var scope = data._scope || this;
    var bNext = true;
    util.forEach(aMainList, function(aPriorityList, nPriority){
      if(aPriorityList){
        util.forEach(aPriorityList, function(oSub, nFuncIndex){
          if(oSub){
            bNext = oSub.call(scope, data);
            if(bNext !== false){
              bNext = true;
            }
            log({exec: topic, priority: nPriority, sort: nFuncIndex, 'continue': bNext});
            return bNext;
          }
        });
        return bNext;
      }
    });
    if(done){
      done();
    }
  },
};

exports.subscribe = events.subscribe;

exports.publish = events.publish;

exports.messages = events.messages;

/**
 * 发布/订阅事件
 *
 * ```subscribe
 * `` topic
 * &` 类型：String<br/>需要订阅的事件，如'cart.get.done'。
 * `` callback
 * &` 类型：Function( publish参数data )<br/>事件发布后的回调函数，回调函数如果^^^return false^^^,则不继续执行该事件的其他回调函数。
 * `` priority
 * &` 类型：Number ( 默认值：10 )<br/>回调函数执行的优先级，SDK默认的优先级为10，如果要先于默认回调执行，请使用1~9的优先级，如果要先于默认回调执行，请大于10的优先级。
 *
 * 如何取消订阅？
 * ^^^
 * <script>
 *   var s = events.subscribe('hello', function(){
 *     console.log('hello');
 *   }); // 成功订阅后会返回引用
 *   //
 *   s.unsubscribe(); //取消订阅
 * </script>
 * ^^^
 * ```
 *
 * ```publish
 * `` topic
 * &` 类型：String<br/>需要发布的事件，如'cart.get.done'。
 * `` data
 * &` 类型：Object
 * &&` res 类型：Object<br/>请不要使用此保留 key，用于 SDK 从 API 获取数据后传至回调函数。
 * &&` anything 类型：Object 选填<br/>使用除 res 之外 key 的对象，可以向订阅事件传送额外数据。
 * ```
 *
 * @param {subscribe} `topic,callback[,priority]` 订阅事件
 * @param {publish} `topic,data` 发布事件
 *
 */<|endoftext|>


<commit_before>// Copyright 2002-2013, University of Colorado Boulder

/**
 * Check box.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var FontAwesomeNode = require( 'SUN/FontAwesomeNode' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Text = require( 'SCENERY/nodes/Text' );
  var AriaSpeech = require( 'SCENERY/accessibility/AriaSpeech' );

  /**
   * @param {Node} content
   * @param {Property.<boolean>} property
   * @constructor
   * @param {Object} [options]
   */
  function CheckBox( content, property, options ) {

    options = _.extend( {
      spacing: 5,
      boxWidth: 21,
      cursor: 'pointer',
      checkBoxColor: 'black',
      checkBoxColorDisabled: 'gray',
      checkBoxColorBackground: 'white',
      tabIndex: 0,
      focusable: true
    }, options );

    var thisNode = this;
    Node.call( this );

    thisNode._checkBoxColor = options.checkBoxColor; // @private
    thisNode._checkBoxColorDisabled = options.checkBoxColorDisabled; // @private
    thisNode._checkBoxColorBackground = options.checkBoxColorBackground; // @private

    // save this stuff for use in prototype functions
    thisNode.options = options; // @private
    thisNode.content = content; // @private
    thisNode._enabled = true; // @private

    // Make the background.  Until we are creating our own shapes, just
    // put a rectangle behind the font awesome check box icons.
    thisNode.backgroundNode = new Rectangle( 0, -options.boxWidth, options.boxWidth * 0.95, options.boxWidth * 0.95,
      options.boxWidth * 0.2, options.boxWidth * 0.2 );

    thisNode.uncheckedNode = new FontAwesomeNode( 'check_empty' ); // @private
    var iconScale = options.boxWidth / thisNode.uncheckedNode.width;
    thisNode.uncheckedNode.scale( iconScale );
    thisNode.checkedNode = new FontAwesomeNode( 'check', { scale: iconScale } ); // @private

    thisNode.addChild( thisNode.backgroundNode );
    thisNode.addChild( thisNode.checkedNode );
    thisNode.addChild( thisNode.uncheckedNode );
    thisNode.addChild( content );

    content.left = thisNode.checkedNode.right + options.spacing;
    content.centerY = thisNode.checkedNode.centerY;

    // put a rectangle on top of everything to prevent dead zones when clicking
    thisNode.addChild( new Rectangle( thisNode.left, thisNode.top, thisNode.width, thisNode.height ) );

    content.pickable = false; // since there's a pickable rectangle on top of content

    // interactivity
    thisNode.addInputListener( new ButtonListener( {
      fire: function() {
        if ( thisNode._enabled ) {
          var oldValue = property.value;
          var newValue = !property.value;
          thisNode.trigger2( 'startedCallbacksForToggled', oldValue, newValue );
          property.value = newValue;
          thisNode.trigger2( 'endedCallbacksForToggled', oldValue, newValue );
        }
      }
    } ) );

    // sync with property
    property.link( function( checked ) {
      thisNode.checkedNode.visible = checked;
      thisNode.uncheckedNode.visible = !checked;
      AriaSpeech.setText( property.value ? 'checked' : 'unchecked' );
    } );

    property.link( function( value ) {
      _.each( thisNode.instances, function( instance ) {

        //Make sure accessibility is enabled, then apply the change to the peer
        _.each( instance.peers, function( peer ) {
          peer.element.setAttribute( 'checked', value );
        } );
      } );
    } );

    this.updateColors();

    // Apply additional options
    thisNode.mutate( options );
  }

  return inherit( Node, CheckBox, {

    // prototype properties

    get checkBoxColorBackground() { return this._checkBoxColorBackground; },
    set checkBoxColorBackground( value ) {
      if ( this._checkBoxColorBackground !== value ) {
        this._checkBoxColorBackground = value;
        this.updateColors();
      }
    },

    get checkBoxColor() { return this._checkBoxColor; },
    set checkBoxColor( value ) {
      if ( this._checkBoxColor !== value ) {
        this._checkBoxColor = value;
        this.updateColors();
      }
    },

    get checkBoxColorDisabled() { return this._checkBoxColorDisabled; },
    set checkBoxColorDisabled( value ) {
      if ( this._checkBoxColorDisabled !== value ) {
        this._checkBoxColorDisabled = value;
        this.updateColors();
      }
    },

    // @private
    updateColors: function() {
      this.backgroundNode.fill = this._checkBoxColorBackground;
      this.checkedNode.fill = this._enabled ? this._checkBoxColor : this._checkBoxColorDisabled;
      this.uncheckedNode.fill = this.checkedNode.fill;
    },

    get enabled() { return this._enabled; },

    set enabled( value ) {

      this._enabled = value;
      this.pickable = value;

      // set the color of the check box icons
      this.updateColors();

      // enable/disable the content, if it supports it
      if ( this.content.setEnabled ) {
        this.content.setEnabled( value );
      }
    }

  }, {

    // static properties

    /**
     * Factory method, creates a check box with a text label and optional icon.
     * @param {string} text
     * @param {*} textOptions options that apply to the text, same as scenery.Text
     * @param {Property.<boolean>} property
     * @returns {CheckBox}
     */
    createTextCheckBox: function( text, textOptions, property, checkBoxOptions ) {

      textOptions = _.extend( {
        fill: 'black',
        fillDisabled: 'rgb(220,220,220)'
      }, textOptions );

      checkBoxOptions = _.extend( {
        icon: null,  // an optional node, added to the right of the text
        iconSpacing: 15
      }, checkBoxOptions );

      var content = new Node();

      // text
      var textNode = new Text( text, textOptions );
      content.addChild( textNode );

      // options icon
      if ( checkBoxOptions.icon ) {
        content.addChild( checkBoxOptions.icon );
        //TODO support different layouts of text and image?
        checkBoxOptions.icon.left = textNode.right + checkBoxOptions.iconSpacing;
        checkBoxOptions.icon.centerY = textNode.centerY;
      }

      content.setEnabled = function( enabled ) {
        textNode.fill = enabled ? textOptions.fill : textOptions.fillDisabled;

        // if the check box has an icon...
        if ( checkBoxOptions.icon ) {
          if ( checkBoxOptions.icon.setEnabled ) {
            // use setEnabled if provided by icon
            checkBoxOptions.icon.setEnabled( enabled );
          }
          else {
            // fallback to using opacity
            checkBoxOptions.icon.opacity = enabled ? 1 : 0.3;
          }
        }
      };

      return new CheckBox( content, property, checkBoxOptions );
    }
  } );
} );<commit_msg>#171 clean up set/get enabled, in preparation for changing implementation<commit_after>// Copyright 2002-2013, University of Colorado Boulder

/**
 * Check box.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var FontAwesomeNode = require( 'SUN/FontAwesomeNode' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Text = require( 'SCENERY/nodes/Text' );
  var AriaSpeech = require( 'SCENERY/accessibility/AriaSpeech' );

  /**
   * @param {Node} content
   * @param {Property.<boolean>} property
   * @constructor
   * @param {Object} [options]
   */
  function CheckBox( content, property, options ) {

    options = _.extend( {
      spacing: 5,
      boxWidth: 21,
      cursor: 'pointer',
      checkBoxColor: 'black',
      checkBoxColorDisabled: 'gray',
      checkBoxColorBackground: 'white',
      tabIndex: 0,
      focusable: true
    }, options );

    var thisNode = this;
    Node.call( this );

    thisNode._checkBoxColor = options.checkBoxColor; // @private
    thisNode._checkBoxColorDisabled = options.checkBoxColorDisabled; // @private
    thisNode._checkBoxColorBackground = options.checkBoxColorBackground; // @private

    // save this stuff for use in prototype functions
    thisNode.options = options; // @private
    thisNode.content = content; // @private
    thisNode._enabled = true; // @private

    // Make the background.  Until we are creating our own shapes, just
    // put a rectangle behind the font awesome check box icons.
    thisNode.backgroundNode = new Rectangle( 0, -options.boxWidth, options.boxWidth * 0.95, options.boxWidth * 0.95,
      options.boxWidth * 0.2, options.boxWidth * 0.2 );

    thisNode.uncheckedNode = new FontAwesomeNode( 'check_empty' ); // @private
    var iconScale = options.boxWidth / thisNode.uncheckedNode.width;
    thisNode.uncheckedNode.scale( iconScale );
    thisNode.checkedNode = new FontAwesomeNode( 'check', { scale: iconScale } ); // @private

    thisNode.addChild( thisNode.backgroundNode );
    thisNode.addChild( thisNode.checkedNode );
    thisNode.addChild( thisNode.uncheckedNode );
    thisNode.addChild( content );

    content.left = thisNode.checkedNode.right + options.spacing;
    content.centerY = thisNode.checkedNode.centerY;

    // put a rectangle on top of everything to prevent dead zones when clicking
    thisNode.addChild( new Rectangle( thisNode.left, thisNode.top, thisNode.width, thisNode.height ) );

    content.pickable = false; // since there's a pickable rectangle on top of content

    // interactivity
    thisNode.addInputListener( new ButtonListener( {
      fire: function() {
        if ( thisNode._enabled ) {
          var oldValue = property.value;
          var newValue = !property.value;
          thisNode.trigger2( 'startedCallbacksForToggled', oldValue, newValue );
          property.value = newValue;
          thisNode.trigger2( 'endedCallbacksForToggled', oldValue, newValue );
        }
      }
    } ) );

    // sync with property
    property.link( function( checked ) {
      thisNode.checkedNode.visible = checked;
      thisNode.uncheckedNode.visible = !checked;
      AriaSpeech.setText( property.value ? 'checked' : 'unchecked' );
    } );

    property.link( function( value ) {
      _.each( thisNode.instances, function( instance ) {

        //Make sure accessibility is enabled, then apply the change to the peer
        _.each( instance.peers, function( peer ) {
          peer.element.setAttribute( 'checked', value );
        } );
      } );
    } );

    this.updateColors();

    // Apply additional options
    thisNode.mutate( options );
  }

  return inherit( Node, CheckBox, {

    // prototype properties

    get checkBoxColorBackground() { return this._checkBoxColorBackground; },
    set checkBoxColorBackground( value ) {
      if ( this._checkBoxColorBackground !== value ) {
        this._checkBoxColorBackground = value;
        this.updateColors();
      }
    },

    get checkBoxColor() { return this._checkBoxColor; },
    set checkBoxColor( value ) {
      if ( this._checkBoxColor !== value ) {
        this._checkBoxColor = value;
        this.updateColors();
      }
    },

    get checkBoxColorDisabled() { return this._checkBoxColorDisabled; },
    set checkBoxColorDisabled( value ) {
      if ( this._checkBoxColorDisabled !== value ) {
        this._checkBoxColorDisabled = value;
        this.updateColors();
      }
    },

    // @private
    updateColors: function() {
      this.backgroundNode.fill = this._checkBoxColorBackground;
      this.checkedNode.fill = this._enabled ? this._checkBoxColor : this._checkBoxColorDisabled;
      this.uncheckedNode.fill = this.checkedNode.fill;
    },

    /**
     * Is the check box enabled?
     * @returns {boolean}
     */
    getEnabled: function() {
      return this._enabled;
    },
    get enabled() { return this.getEnabled(); },

    /**
     * Sets whether the check box is enabled.
     * @param {boolean} value
     */
    setEnabled: function( value ) {

      this._enabled = value;
      this.pickable = value;

      // set the color of the check box icons
      this.updateColors();

      // enable/disable the content, if it supports it
      if ( this.content.setEnabled ) {
        this.content.setEnabled( value );
      }
    },
    set enabled( value ) { this.setEnabled( value ); }

  }, {

    // static properties

    /**
     * Factory method, creates a check box with a text label and optional icon.
     * @param {string} text
     * @param {*} textOptions options that apply to the text, same as scenery.Text
     * @param {Property.<boolean>} property
     * @returns {CheckBox}
     * @static
     */
    createTextCheckBox: function( text, textOptions, property, checkBoxOptions ) {

      textOptions = _.extend( {
        fill: 'black',
        fillDisabled: 'rgb(220,220,220)'
      }, textOptions );

      checkBoxOptions = _.extend( {
        icon: null,  // an optional node, added to the right of the text
        iconSpacing: 15
      }, checkBoxOptions );

      var content = new Node();

      // text
      var textNode = new Text( text, textOptions );
      content.addChild( textNode );

      // options icon
      if ( checkBoxOptions.icon ) {
        content.addChild( checkBoxOptions.icon );
        //TODO support different layouts of text and image?
        checkBoxOptions.icon.left = textNode.right + checkBoxOptions.iconSpacing;
        checkBoxOptions.icon.centerY = textNode.centerY;
      }

      content.setEnabled = function( enabled ) {
        textNode.fill = enabled ? textOptions.fill : textOptions.fillDisabled;

        // if the check box has an icon...
        if ( checkBoxOptions.icon ) {
          if ( checkBoxOptions.icon.setEnabled ) {
            // use setEnabled if provided by icon
            checkBoxOptions.icon.setEnabled( enabled );
          }
          else {
            // fallback to using opacity
            checkBoxOptions.icon.opacity = enabled ? 1 : 0.3;
          }
        }
      };

      return new CheckBox( content, property, checkBoxOptions );
    }
  } );
} );<|endoftext|>


<commit_before>Package.describe({
  name: 'harrison:papa-parse',
  summary: 'A powerful, in-browser CSV parser for big boys and girls',
  version: '1.0.0',
  git: ' /* Fill me in! */ '
});

Package.onUse(function(api) {
  api.versionsFrom('undefined');
  api.addFiles('harrison:papa-parse.js', ['client']);
});

Package.onTest(function(api) {
  api.use('tinytest');
  api.use('harrison:papa-parse');
  api.addFiles('harrison:papa-parse-tests.js', ['client']);
});
<commit_msg>updated package<commit_after>Package.describe({
  name: 'harrison:papa-parse',
  summary: 'A powerful, in-browser CSV parser for big boys and girls',
  version: '1.0.0',
  git: 'https://github.com/harrisonhunter/papa-parse-meteor.git'
});

Package.onUse(function(api) {
  api.addFiles('harrison:papa-parse.js', ['client']);
});

Package.onTest(function(api) {
  api.use('tinytest');
  api.use('harrison:papa-parse');
  api.addFiles('harrison:papa-parse-tests.js', ['client']);
});
<|endoftext|>


<commit_before><commit_msg>Verify with callback<commit_after><|endoftext|>


<commit_before>var _ = require('lodash');

exports.getLinks = function getLinks(req, res) {
  var results = ['url\tlastResTime\tavgResTime\tcount'];
  var tmpl = _.template('<%=url%>\t<%=lastResTime%>\t<%=avgResTime%>\t<%=count%>');

  req.app.get('models').Link.fetchAll()
    .on('data', function(aLink) {
      results.push(tmpl(aLink.value));
    })
    .on('end', function() {
      res.type('.txt').send(results.join('\n') + '\n');
    })
    .on('err', function(err) {
      console.error(err);
    });
};

exports.create = function create(req, res) {
  var models = req.app.get('models');
  models.Link.create(req.body, function(err) {
    if (err) {
      res.status(400).json({
        message: err.toString()
      });
    }
    res.status(201).send(void 0);
  });
};
<commit_msg>Output proxy info of links<commit_after>var _ = require('lodash');

exports.getLinks = function getLinks(req, res) {
  var results = ['url\t\tproxy\t\tlastResTime\tavgResTime\tcount'];
  var tmpl = _.template('<%=url%>\t<%=proxy%>\t<%=lastResTime%>\t<%=avgResTime%>\t<%=count%>');

  req.app.get('models').Link.fetchAll()
    .on('data', function(aLink) {
      results.push(
        tmpl(
          _.defaults(
            aLink.value, { proxy: ''}
          )
        )
      );
    })
    .on('end', function() {
      res.type('.txt').send(results.join('\n') + '\n');
    })
    .on('err', function(err) {
      console.error(err);
    });
};

exports.create = function create(req, res) {
  var models = req.app.get('models');
  models.Link.create(req.body, function(err) {
    if (err) {
      res.status(400).json({
        message: err.toString()
      });
    }
    res.status(201).send(void 0);
  });
};
<|endoftext|>


<commit_before>/**
 * view.js
 */

var util = require('util'),
    EventEmitter = require('events').EventEmitter;

var View = function() {

}

util.inherits(View, EventEmitter);<commit_msg>Removed view<commit_after><|endoftext|>


<commit_before>// Copyright 2002-2013, University of Colorado Boulder

/**
 * Scenery-based combo box. Composed of a button and a list of items.
 * The list of items is displayed when the button is pressed, and dismissed an item is selected
 * or the user clicks outside the list.  The list can be displayed either above or below the button.
 *

 * An item in the combo box had 2 properties: {Node} node, {*} value.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var inherit = require( 'PHET_CORE/inherit' );
  var Line = require( 'SCENERY/nodes/Line' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var Vector2 = require( 'DOT/Vector2' );

  /**
   * The button that is clicked to show the list of items.
   * @param {Node} itemNode
   * @param {Object} [options] object with optional properties
   * @constructor
   */
  function ButtonNode( itemNode, options ) {
    Node.call( this );
    this.componentID = null;
    this.componentType = null;

    // up or down arrow
    var arrow = new Path( null, { fill: 'black' } );
    var arrowWidth = 0.5 * itemNode.height;
    var arrowHeight = arrowWidth * Math.sqrt( 3 ) / 2; // height of equilateral triangle
    if ( options.listPosition === 'above' ) {
      arrow.shape = new Shape().moveTo( 0, arrowHeight ).lineTo( arrowWidth / 2, 0 ).lineTo( arrowWidth, arrowHeight ).close(); // up arrow
    }
    else {
      arrow.shape = new Shape().moveTo( 0, 0 ).lineTo( arrowWidth, 0 ).lineTo( arrowWidth / 2, arrowHeight ).close(); // down arrow
    }

    // button background
    var width = itemNode.width + ( 4 * options.buttonXMargin ) + arrow.width;
    var height = itemNode.height + ( 2 * options.buttonYMargin );
    var background = new Rectangle( 0, 0, width, height, options.buttonCornerRadius, options.buttonCornerRadius,
      { fill: options.buttonFill, stroke: options.buttonStroke, lineWidth: options.buttonLineWidth } );

    // vertical separator to left of arrow
    var separator = new Line( 0, 0, 0, height, { stroke: 'black', lineWidth: options.buttonLineWidth } );

    // itemNode's parent
    var itemNodeParent = new Node();

    // rendering order
    this.addChild( background );
    this.addChild( arrow );
    this.addChild( separator );
    this.addChild( itemNodeParent );

    this.setItemNode = function( itemNode ) {
      itemNodeParent.removeAllChildren();
      itemNodeParent.addChild( itemNode );
      itemNode.left = options.buttonXMargin;
      itemNode.top = options.buttonYMargin;
    };
    this.setItemNode( itemNode );

    // layout
    separator.left = itemNode.right + options.buttonXMargin;
    separator.top = background.top;
    arrow.left = separator.right + options.buttonXMargin;
    arrow.centerY = background.centerY;
  }

  inherit( Node, ButtonNode );

  /**
   * A wrapper around the combo box item, adds margins, etc.
   * @param item
   * @param {number} width
   * @param {number} height
   * @param {number} xMargin
   * @constructor
   */
  function ItemNode( item, width, height, xMargin ) {
    var thisNode = this;
    Rectangle.call( this, 0, 0, width, height );
    this.item = item;
    thisNode.addChild( item.node );
    item.node.pickable = false; // hits will occur on the rectangle
    item.node.x = xMargin;
    item.node.centerY = height / 2;
  }

  inherit( Rectangle, ItemNode );

  /**
   * @param {Array} items
   * @param {*} property
   * @param {Node} listParent node that will be used as the list's parent, use this to ensuring that the list is in front of everything else
   * @param {Object} [options] object with optional properties
   */
  function ComboBox( items, property, listParent, options ) {

    var self = this;

    options = _.extend( {
        labelNode: null, // optional label, placed to the left of the combo box
        labelXSpacing: 10, // horizontal space between label and combo box
        // button
        buttonFill: 'white',
        buttonStroke: 'black',
        buttonLineWidth: 1,
        buttonCornerRadius: 8,
        buttonXMargin: 10,
        buttonYMargin: 4,
        // list
        listPosition: 'below', // where the list is positioned relative to the button, either 'below' or 'above'
        listYMargin: 4,
        listFill: 'white',
        listStroke: 'black',
        listLineWidth: 1,
        listCornerRadius: 5,
        // items
        itemXMargin: 6,
        itemYMargin: 6,
        itemHighlightFill: 'rgb(245,245,245)',
        itemHighlightStroke: null,
        itemHighlightLineWidth: 1
      },
      options );

    Node.call( self );

    // optional label
    if ( options.labelNode !== null ) {
      self.addChild( options.labelNode );
    }

    // determine uniform dimensions for button and list items
    var itemWidth = 0, itemHeight = 0;
    for ( var i = 0; i < items.length; i++ ) {
      var item = items[ i ];
      if ( item.node.width > itemWidth ) { itemWidth = item.node.width; }
      if ( item.node.height > itemHeight ) { itemHeight = item.node.height; }
    }
    itemWidth += ( 2 * options.itemXMargin );
    itemHeight += ( 2 * options.itemYMargin );

    // button, will be set to correct value when property observer is registered
    var buttonNode = new ButtonNode( new ItemNode( items[ 0 ], itemWidth, itemHeight, options.itemXMargin ), options );
    self.addChild( buttonNode );

    // list
    var listWidth = itemWidth + ( 2 * options.buttonXMargin );
    var listHeight = ( items.length * itemHeight ) + ( 2 * options.listYMargin );
    var listNode = new Rectangle( 0, 0, listWidth, listHeight, options.listCornerRadius, options.listCornerRadius,
      { fill: options.listFill, stroke: options.listStroke, lineWidth: options.listLineWidth, visible: false } );
    listParent.addChild( listNode );

    //TODO move these to ItemNode
    // how to highlight an item in the list
    var highlightItem = function( itemNode ) {
      itemNode.fill = options.itemHighlightFill;
      itemNode.stroke = options.itemHighlightStroke;
    };
    var unhighlightItem = function( itemNode ) {
      itemNode.fill = null;
      itemNode.stroke = null;
    };

    // listener that we'll attach to each item in the list
    var itemListener = {
      enter: function( event ) {
        highlightItem( event.currentTarget );
      },
      exit: function( event ) {
        unhighlightItem( event.currentTarget );
      },
      down: function( event ) {
        event.abort(); // prevent click-to-dismiss on the list
      },
      up: function( event ) {

        var archID = arch && arch.start( 'user', event.currentTarget.componentID, event.currentTarget.componentType, 'pressed' );

        unhighlightItem( event.currentTarget );
        listNode.visible = false; // close the list, do this before changing property value, in case it's expensive
        self.getUniqueTrail().rootNode().removeInputListener( clickToDismissListener ); // remove the click-to-dismiss listener
        event.abort(); // prevent nodes (eg, controls) behind the list from receiving the event
        property.value = event.currentTarget.item.value; // set the property

        arch && arch.end( archID );
      }
    };

    // populate list with items
    for ( var j = 0; j < items.length; j++ ) {
      // add item to list
      var itemNode = new ItemNode( items[ j ], itemWidth, itemHeight, options.itemXMargin );
      listNode.addChild( itemNode );
      itemNode.left = options.buttonXMargin;
      itemNode.top = options.listYMargin + ( j * itemHeight );

      // item interactivity
      itemNode.cursor = 'pointer';
      itemNode.addInputListener( itemListener );
    }

    //TODO handle scale and rotation
    // Handles the coordinate transform required to make the list pop up near the button.
    var moveList = function() {
      var pButtonGlobal, pButtonLocal;
      if ( options.listPosition === 'above' ) {
        pButtonGlobal = self.localToGlobalPoint( new Vector2( buttonNode.left, buttonNode.top ) );
        pButtonLocal = listParent.globalToLocalPoint( pButtonGlobal );
        listNode.left = pButtonLocal.x;
        listNode.bottom = pButtonLocal.y;
      }
      else {
        pButtonGlobal = self.localToGlobalPoint( new Vector2( buttonNode.left, buttonNode.bottom ) );
        pButtonLocal = listParent.globalToLocalPoint( pButtonGlobal );
        listNode.left = pButtonLocal.x;
        listNode.top = pButtonLocal.y;
      }
    };

    /**
     * Because clickToDismissListener is added to the scene, it receives the 'down' event that
     * buttonNode received to register the listener. This is because scenery propagates events
     * up the event trail, and the scene is further up the trail than the button.  This flag
     * is used to ignore the first 'down' event, which is the one that the button received.
     * If we don't do this, then we never see the list because it is immediately popped down.
     * This behavior is may change, and is being discussed in scenery#58.
     */
    var enableClickToDismissListener;
    var sceneNode; // store the node that clickToDismissListener is added to, because the scene may change, see sun#14

    // listener for 'click outside to dismiss'
    var clickToDismissListener = {
      down: function() {
        if ( enableClickToDismissListener ) {

          var archID = arch && arch.start( 'user', 'scene', 'scene', 'pressed', { comboBoxPopup: 'hidden' } );

          sceneNode.removeInputListener( clickToDismissListener );
          listNode.visible = false;

          arch && arch.end( archID );
        }
        else {
          enableClickToDismissListener = true;
        }
      }
    };

    // button interactivity
    buttonNode.cursor = 'pointer';
    buttonNode.addInputListener(
      {
        down: function() {
          if ( !listNode.visible ) {
            var archID = arch && arch.start( 'user', buttonNode.componentID, buttonNode.componentType, 'pressed' );

            moveList();
            listNode.visible = true;
            enableClickToDismissListener = false;
            sceneNode = self.getUniqueTrail().rootNode();
            sceneNode.addInputListener( clickToDismissListener );

            arch && arch.end( archID );
          }
        }
      } );

    // layout
    if ( options.labelNode ) {
      buttonNode.left = options.labelNode.right + options.labelXSpacing;
      buttonNode.centerY = options.labelNode.centerY;
    }

    // when property changes, update button
    property.link( function( value ) {
      // TODO brute force search, better way?
      var item = null;
      for ( var i = 0; i < items.length; i++ ) {
        if ( items[ i ].value === value ) {
          item = items[ i ];
        }
      }
      assert && assert( item !== null );
      buttonNode.setItemNode( new ItemNode( item, itemWidth, itemHeight, options.itemXMargin ) );
    } );

    this.mutate( options );
  }

  inherit( Node, ComboBox );

  /**
   * Creates a combo box item.
   * This exists primarily to document the structure of an item.
   * @param {Node} node
   * @param {*} value
   * @returns {{node: *, value: *}}
   */
  ComboBox.createItem = function( node, value ) {
    return { node: node, value: value };
  };

  return ComboBox;
} );
<commit_msg>Register the ComboBox buttonNode with together, see #162<commit_after>// Copyright 2002-2013, University of Colorado Boulder

/**
 * Scenery-based combo box. Composed of a button and a list of items.
 * The list of items is displayed when the button is pressed, and dismissed an item is selected
 * or the user clicks outside the list.  The list can be displayed either above or below the button.
 *

 * An item in the combo box had 2 properties: {Node} node, {*} value.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var inherit = require( 'PHET_CORE/inherit' );
  var Line = require( 'SCENERY/nodes/Line' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var Vector2 = require( 'DOT/Vector2' );

  /**
   * The button that is clicked to show the list of items.
   * @param {Node} itemNode
   * @param {Object} [options] object with optional properties
   * @constructor
   */
  function ButtonNode( itemNode, options ) {
    Node.call( this );
    this.componentID = null;
    this.componentType = null;

    // up or down arrow
    var arrow = new Path( null, { fill: 'black' } );
    var arrowWidth = 0.5 * itemNode.height;
    var arrowHeight = arrowWidth * Math.sqrt( 3 ) / 2; // height of equilateral triangle
    if ( options.listPosition === 'above' ) {
      arrow.shape = new Shape().moveTo( 0, arrowHeight ).lineTo( arrowWidth / 2, 0 ).lineTo( arrowWidth, arrowHeight ).close(); // up arrow
    }
    else {
      arrow.shape = new Shape().moveTo( 0, 0 ).lineTo( arrowWidth, 0 ).lineTo( arrowWidth / 2, arrowHeight ).close(); // down arrow
    }

    // button background
    var width = itemNode.width + ( 4 * options.buttonXMargin ) + arrow.width;
    var height = itemNode.height + ( 2 * options.buttonYMargin );
    var background = new Rectangle( 0, 0, width, height, options.buttonCornerRadius, options.buttonCornerRadius,
      { fill: options.buttonFill, stroke: options.buttonStroke, lineWidth: options.buttonLineWidth } );

    // vertical separator to left of arrow
    var separator = new Line( 0, 0, 0, height, { stroke: 'black', lineWidth: options.buttonLineWidth } );

    // itemNode's parent
    var itemNodeParent = new Node();

    // rendering order
    this.addChild( background );
    this.addChild( arrow );
    this.addChild( separator );
    this.addChild( itemNodeParent );

    this.setItemNode = function( itemNode ) {
      itemNodeParent.removeAllChildren();
      itemNodeParent.addChild( itemNode );
      itemNode.left = options.buttonXMargin;
      itemNode.top = options.buttonYMargin;
    };
    this.setItemNode( itemNode );

    // layout
    separator.left = itemNode.right + options.buttonXMargin;
    separator.top = background.top;
    arrow.left = separator.right + options.buttonXMargin;
    arrow.centerY = background.centerY;
  }

  inherit( Node, ButtonNode );

  /**
   * A wrapper around the combo box item, adds margins, etc.
   * @param item
   * @param {number} width
   * @param {number} height
   * @param {number} xMargin
   * @constructor
   */
  function ItemNode( item, width, height, xMargin ) {
    var thisNode = this;
    Rectangle.call( this, 0, 0, width, height );
    this.item = item;
    thisNode.addChild( item.node );
    item.node.pickable = false; // hits will occur on the rectangle
    item.node.x = xMargin;
    item.node.centerY = height / 2;
  }

  inherit( Rectangle, ItemNode );

  /**
   * @param {Array} items
   * @param {*} property
   * @param {Node} listParent node that will be used as the list's parent, use this to ensuring that the list is in front of everything else
   * @param {Object} [options] object with optional properties
   */
  function ComboBox( items, property, listParent, options ) {

    var self = this;

    options = _.extend( {
        labelNode: null, // optional label, placed to the left of the combo box
        labelXSpacing: 10, // horizontal space between label and combo box
        // button
        buttonFill: 'white',
        buttonStroke: 'black',
        buttonLineWidth: 1,
        buttonCornerRadius: 8,
        buttonXMargin: 10,
        buttonYMargin: 4,
        // list
        listPosition: 'below', // where the list is positioned relative to the button, either 'below' or 'above'
        listYMargin: 4,
        listFill: 'white',
        listStroke: 'black',
        listLineWidth: 1,
        listCornerRadius: 5,
        // items
        itemXMargin: 6,
        itemYMargin: 6,
        itemHighlightFill: 'rgb(245,245,245)',
        itemHighlightStroke: null,
        itemHighlightLineWidth: 1,
        //together
        buttonNodeComponentID: null // Must be specified if using together
      },
      options );

    Node.call( self );

    // optional label
    if ( options.labelNode !== null ) {
      self.addChild( options.labelNode );
    }

    // determine uniform dimensions for button and list items
    var itemWidth = 0, itemHeight = 0;
    for ( var i = 0; i < items.length; i++ ) {
      var item = items[ i ];
      if ( item.node.width > itemWidth ) { itemWidth = item.node.width; }
      if ( item.node.height > itemHeight ) { itemHeight = item.node.height; }
    }
    itemWidth += ( 2 * options.itemXMargin );
    itemHeight += ( 2 * options.itemYMargin );

    // button, will be set to correct value when property observer is registered
    var buttonNode = new ButtonNode( new ItemNode( items[ 0 ], itemWidth, itemHeight, options.itemXMargin ), options );
    self.addChild( buttonNode );

    // list
    var listWidth = itemWidth + ( 2 * options.buttonXMargin );
    var listHeight = ( items.length * itemHeight ) + ( 2 * options.listYMargin );
    var listNode = new Rectangle( 0, 0, listWidth, listHeight, options.listCornerRadius, options.listCornerRadius,
      { fill: options.listFill, stroke: options.listStroke, lineWidth: options.listLineWidth, visible: false } );
    listParent.addChild( listNode );

    //TODO move these to ItemNode
    // how to highlight an item in the list
    var highlightItem = function( itemNode ) {
      itemNode.fill = options.itemHighlightFill;
      itemNode.stroke = options.itemHighlightStroke;
    };
    var unhighlightItem = function( itemNode ) {
      itemNode.fill = null;
      itemNode.stroke = null;
    };

    // listener that we'll attach to each item in the list
    var itemListener = {
      enter: function( event ) {
        highlightItem( event.currentTarget );
      },
      exit: function( event ) {
        unhighlightItem( event.currentTarget );
      },
      down: function( event ) {
        event.abort(); // prevent click-to-dismiss on the list
      },
      up: function( event ) {

        var archID = arch && arch.start( 'user', event.currentTarget.componentID, event.currentTarget.componentType, 'pressed' );

        unhighlightItem( event.currentTarget );
        listNode.visible = false; // close the list, do this before changing property value, in case it's expensive
        self.getUniqueTrail().rootNode().removeInputListener( clickToDismissListener ); // remove the click-to-dismiss listener
        event.abort(); // prevent nodes (eg, controls) behind the list from receiving the event
        property.value = event.currentTarget.item.value; // set the property

        arch && arch.end( archID );
      }
    };

    // populate list with items
    for ( var j = 0; j < items.length; j++ ) {
      // add item to list
      var itemNode = new ItemNode( items[ j ], itemWidth, itemHeight, options.itemXMargin );
      listNode.addChild( itemNode );
      itemNode.left = options.buttonXMargin;
      itemNode.top = options.listYMargin + ( j * itemHeight );

      // item interactivity
      itemNode.cursor = 'pointer';
      itemNode.addInputListener( itemListener );
    }

    //TODO handle scale and rotation
    // Handles the coordinate transform required to make the list pop up near the button.
    var moveList = function() {
      var pButtonGlobal, pButtonLocal;
      if ( options.listPosition === 'above' ) {
        pButtonGlobal = self.localToGlobalPoint( new Vector2( buttonNode.left, buttonNode.top ) );
        pButtonLocal = listParent.globalToLocalPoint( pButtonGlobal );
        listNode.left = pButtonLocal.x;
        listNode.bottom = pButtonLocal.y;
      }
      else {
        pButtonGlobal = self.localToGlobalPoint( new Vector2( buttonNode.left, buttonNode.bottom ) );
        pButtonLocal = listParent.globalToLocalPoint( pButtonGlobal );
        listNode.left = pButtonLocal.x;
        listNode.top = pButtonLocal.y;
      }
    };

    /**
     * Because clickToDismissListener is added to the scene, it receives the 'down' event that
     * buttonNode received to register the listener. This is because scenery propagates events
     * up the event trail, and the scene is further up the trail than the button.  This flag
     * is used to ignore the first 'down' event, which is the one that the button received.
     * If we don't do this, then we never see the list because it is immediately popped down.
     * This behavior is may change, and is being discussed in scenery#58.
     */
    var enableClickToDismissListener;
    var sceneNode; // store the node that clickToDismissListener is added to, because the scene may change, see sun#14

    // listener for 'click outside to dismiss'
    var clickToDismissListener = {
      down: function() {
        if ( enableClickToDismissListener ) {

          var archID = arch && arch.start( 'user', 'scene', 'scene', 'pressed', { comboBoxPopup: 'hidden' } );

          sceneNode.removeInputListener( clickToDismissListener );
          listNode.visible = false;

          arch && arch.end( archID );
        }
        else {
          enableClickToDismissListener = true;
        }
      }
    };

    // button interactivity
    buttonNode.cursor = 'pointer';
    buttonNode.addInputListener(
      {
        down: function() {
          if ( !listNode.visible ) {
            var archID = arch && arch.start( 'user', buttonNode.componentID, buttonNode.componentType, 'pressed' );

            moveList();
            listNode.visible = true;
            enableClickToDismissListener = false;
            sceneNode = self.getUniqueTrail().rootNode();
            sceneNode.addInputListener( clickToDismissListener );

            arch && arch.end( archID );
          }
        }
      } );

    // layout
    if ( options.labelNode ) {
      buttonNode.left = options.labelNode.right + options.labelXSpacing;
      buttonNode.centerY = options.labelNode.centerY;
    }

    // when property changes, update button
    property.link( function( value ) {
      // TODO brute force search, better way?
      var item = null;
      for ( var i = 0; i < items.length; i++ ) {
        if ( items[ i ].value === value ) {
          item = items[ i ];
        }
      }
      assert && assert( item !== null );
      buttonNode.setItemNode( new ItemNode( item, itemWidth, itemHeight, options.itemXMargin ) );
    } );

    this.mutate( options );

    together && together.addComponent( options.buttonNodeComponentID, buttonNode );
  }

  inherit( Node, ComboBox );

  /**
   * Creates a combo box item.
   * This exists primarily to document the structure of an item.
   * @param {Node} node
   * @param {*} value
   * @returns {{node: *, value: *}}
   */
  ComboBox.createItem = function( node, value ) {
    return { node: node, value: value };
  };

  return ComboBox;
} );
<|endoftext|>


<commit_before>Package.describe({
  git: 'https://github.com/zimme/meteor-collection-softremovable.git',
  name: 'zimme:collection-softremovable',
  summary: 'Add soft remove to collections',
  version: '1.0.4-rc.1'
});

Package.onUse(function(api) {
  api.versionsFrom('1.0');

  api.use([
    'coffeescript',
    'underscore'
  ]);

  api.use([
    'matb33:collection-hooks@0.7.6',
    'zimme:collection-behaviours@1.0.3'
  ]);

  api.use([
    'aldeed:autoform@4.0.0 || 5.0.0',
    'aldeed:collection2@2.0.0',
    'aldeed:simple-schema@1.0.3'
  ], ['client', 'server'], {weak: true});

  api.imply('zimme:collection-behaviours');

  api.addFiles('softremovable.coffee');
});
<commit_msg>Version bump<commit_after>Package.describe({
  git: 'https://github.com/zimme/meteor-collection-softremovable.git',
  name: 'zimme:collection-softremovable',
  summary: 'Add soft remove to collections',
  version: '1.0.4'
});

Package.onUse(function(api) {
  api.versionsFrom('1.0');

  api.use([
    'coffeescript',
    'underscore'
  ]);

  api.use([
    'matb33:collection-hooks@0.7.6',
    'zimme:collection-behaviours@1.0.3'
  ]);

  api.use([
    'aldeed:autoform@4.0.0 || 5.0.0',
    'aldeed:collection2@2.0.0',
    'aldeed:simple-schema@1.0.3'
  ], ['client', 'server'], {weak: true});

  api.imply('zimme:collection-behaviours');

  api.addFiles('softremovable.coffee');
});
<|endoftext|>