{"action":"created","comment":{"url":"https://api.github.com/repos/metamx/locators/pulls/comments/75997345","id":75997345,"diff_hunk":"@@ -1,316 +1,432 @@\n-\n+import {EventEmitter} from \"events\";\n import http = require(\"http\");\n-import EventEmitterModule = require(\"events\");\n-import EventEmitter = EventEmitterModule.EventEmitter;\n-import async = require(\"async\");\n-import Promise = require(\"q\");\n import zookeeper = require(\"node-zookeeper-client\");\n-import Client = zookeeper.Client;\n-var Exception = zookeeper.Exception;\n+import Promise = require(\"bluebird\");\n \n-import { Location, Locator, DataExtractor } from \"./common\";\n+import {Location, Locator, LocatorEmitter, LocatorFactory, DataExtractor} from \"./common\";\n import LocatorException = require(\"./locatorException\");\n \n+function deferredLocationRequest() : Promise.Resolver<Location> {\n+    var resolve, reject;\n+    var promise = new Promise<Location>(function() {\n+        resolve = arguments[0];\n+        reject = arguments[1];\n+    });\n+    return {\n+        resolve: resolve,\n+        reject: reject,\n+        promise: promise,\n+        callback: null\n+    };\n+}\n \n interface ZookeeperJS {\n-  address : string;\n-  port : number;\n+    address : string;\n+    port : number;\n }\n \n+export interface ZookeeperLocatorParameters {\n+    serverLocator : Locator;\n+    path : string;\n+    dataExtractor? : DataExtractor;\n+    locatorTimeout? : number;\n+    sessionTimeout? : number;\n+    spinDelay? : number;\n+    retries? : number;\n+    strict? : boolean;\n+}\n \n-class ClientWrapper {\n-  public client : Client;\n-  public emitter : EventEmitter;\n \n-  constructor() {\n-    this.emitter = new EventEmitter();\n-    this.client = null;\n-  }\n+export class ZookeeperLocator {\n \n-  public setClient(client : Client) {\n-    this.client = client;\n-    this.emitter.emit('NEW_CLIENT');\n-  }\n-}\n+    static getLocatorFactory(parameters : ZookeeperLocatorParameters) : LocatorFactory {\n+        let manager = new ZookeeperLocatorFatory(parameters);\n+        return manager.getLocatorForPath;\n+    };\n \n-function defaultDataExtractor(data : string) : Location {\n-  var zkJS : ZookeeperJS;\n-  try {\n-    zkJS = JSON.parse(data);\n-  } catch (e) {\n-    return null;\n-  }\n-\n-  if (!zkJS.address || !zkJS.port) {\n-    return null;\n-  }\n-\n-  return {\n-    host: zkJS.address,\n-    port: zkJS.port\n-  };\n-}\n+    static DefaultLocatorTimeout : number = 2000;\n+\n+    static DefaultSessionTimeout : number = 10000;\n+\n+    static DefaultSpinDelay : number = 0;\n+\n+    static DefaultRetries : number = 0;\n+\n+    static DefaultDataExtractor : DataExtractor = (data : string) : Location => {\n+        var zkJS : ZookeeperJS;\n+        try {\n+            zkJS = JSON.parse(data);\n+        } catch (e) {\n+            return null;\n+        }\n+\n+        if (!zkJS.address || !zkJS.port) {\n+            return null;\n+        }\n \n-function defaultServerExtractor(data : string) : string[] {\n-  return JSON.parse(data);\n+        return {\n+            host: zkJS.address,\n+            port: zkJS.port\n+        };\n+    };\n }\n \n-// TODO: turn manager into a class\n-function makeManagerForPath(clientWrapper : ClientWrapper,\n-                            path : string,\n-                            emitter : EventEmitter,\n-                            dataExtractor : DataExtractor,\n-                            locatorTimeout : number,\n-                            strict : boolean) : Locator {\n-  var next = -1;\n-  var pool : Location[] = null;\n-  var cachedPool : Location[] = null;\n-  var queue : Promise.Deferred<Location>[] = [];\n-  var stale = false;\n-\n-  function setPool(newPool : Location[]) {\n-    pool = newPool;\n-    if (!cachedPool || (newPool && newPool.length)) {\n-      cachedPool = newPool;\n-    }\n-    return;\n-  }\n \n-  function getPool() : Location[] {\n-    if (strict || (pool && pool.length)) {\n-      return pool;\n-    }\n-    if (cachedPool && cachedPool.length) {\n-      return cachedPool;\n-    }\n-    return pool;\n-  }\n+/**\n+ * ZookeeperLocatorFactory manages a zookeeper client connection as well as constructing\n+ * ZookeeperPathLocatorFactories for each path requests, passing them a handle to the zookeeper\n+ * client via ZookeeperClientWrapper\n+ */\n+class ZookeeperLocatorFatory extends EventEmitter {\n+    private client : zookeeper.Client;\n+    private clientWrapper : ZookeeperClientWrapper;\n+    private serverLocator : Locator;\n+    private discoveryPath : string;\n+    private dataExtractor : DataExtractor;\n+    private locatorTimeout : number;\n+    private sessionTimeout : number;\n+    private spinDelay : number;\n+    private retries : number;\n+    private strict : boolean;\n+    private pathLocatorFactories : { [key : string] : ZookeeperPathLocatorFactory};\n+\n+    /**\n+     * given a path in a zookeeper cluster, return a Locator to resolve to a Location listed in that path\n+     * @param path\n+     * @returns {Locator}\n+     */\n+    getLocatorForPath = (path : string) : LocatorEmitter => {\n+        if (typeof path !== \"string\") {\n+            throw new TypeError(\"path must be a string\");\n+        }\n+        if (path[0] !== \"/\") {\n+            path = \"/\" + path;\n+        }\n \n-  function dispatch(deferred : Promise.Deferred<Location>) {\n-    var chosenPool = getPool();\n-    if (!chosenPool) {\n-      throw new Error(\"get next called on loading pool\");\n-    }\n+        if (!this.pathLocatorFactories[path]) {\n+            this.pathLocatorFactories[path] = new ZookeeperPathLocatorFactory(\n+                this.clientWrapper,\n+                path,\n+                this,\n+                this.dataExtractor,\n+                this.locatorTimeout,\n+                this.strict\n+            );\n+        }\n \n-    if (chosenPool.length) {\n-      next++;\n-      return deferred.resolve(chosenPool[next % chosenPool.length]);\n-    } else {\n-      return deferred.reject(new Error(LocatorException.CODE[\"EMPTY_POOL\"]));\n+        return <LocatorEmitter>(<any>this.pathLocatorFactories[path].getLocator);\n+    };\n+\n+    constructor(parameters : ZookeeperLocatorParameters) {\n+        super();\n+        let { serverLocator, path, dataExtractor, locatorTimeout, sessionTimeout, spinDelay, retries, strict } = parameters;\n+\n+        this.serverLocator = serverLocator;\n+        this.discoveryPath = path;\n+        this.dataExtractor = dataExtractor ? dataExtractor : ZookeeperLocator.DefaultDataExtractor;\n+        this.locatorTimeout = locatorTimeout ? locatorTimeout : ZookeeperLocator.DefaultLocatorTimeout;\n+        this.sessionTimeout = sessionTimeout ? sessionTimeout : ZookeeperLocator.DefaultSessionTimeout;\n+        this.spinDelay = spinDelay ? spinDelay : ZookeeperLocator.DefaultSpinDelay;\n+        this.retries = retries ? retries : ZookeeperLocator.DefaultRetries;\n+        this.strict = strict == null ? true : strict;\n+\n+        this.clientWrapper = new ZookeeperClientWrapper();\n+        this.pathLocatorFactories = {};\n+\n+        // wire up wrapped event emitter to function getLocatorForPath to 'implement' the Locator interface\n+        // ugly, but this whole shared event emitter business sort of is :(\n+        // also, how only the zookeeper locator implements this :(\n+        Object.keys(EventEmitter.prototype).forEach((fnName) =>\n+            (<any>this.getLocatorForPath)[fnName] = this[fnName]\n+        );\n+\n+        this.connect();\n     }\n-  }\n \n-  function processQueue() {\n-    while (queue.length) {\n-      dispatch(queue.shift());\n-    }\n-  }\n-\n-  function onGetChildren(err : Error, children : string[]) {\n-    if (err) {\n-      emitter.emit(LocatorException.CODE[\"FAILED_TO_GET_CHILDREN\"], path, err);\n-      setPool([]);\n-      processQueue();\n-      return;\n+    /**\n+     * Connect to the zookeeper cluster, rebuilding the client on session expired.\n+     */\n+    public connect() {\n+        this.serverLocator()\n+            .then((location) => {\n+                var zookeeperServer = location.host;\n+                if (location.port) {\n+                    zookeeperServer = zookeeperServer + \":\" + location.port;\n+                }\n+                zookeeperServer = zookeeperServer + this.discoveryPath;\n+\n+                if (this.client) {\n+                    this.client.removeAllListeners();\n+                }\n+\n+                this.client = zookeeper.createClient(zookeeperServer, {\n+                    sessionTimeout: this.sessionTimeout,\n+                    spinDelay: this.spinDelay,\n+                    retries: this.retries\n+                });\n+\n+                this.clientWrapper.setClient(this.client);\n+\n+                var disconnectedHandler = null;\n+\n+                this.client.on(\"connected\", () => {\n+                    this.emit(LocatorException.CODE.CONNECTED);\n+\n+                    if (disconnectedHandler) {\n+                        clearTimeout(disconnectedHandler);\n+                    }\n+                });\n+                this.client.on(\"disconnected\", (count : number) => {\n+                    this.emit(LocatorException.CODE.DISCONNECTED);\n+\n+                    disconnectedHandler = setTimeout(\n+                        () => {\n+                            this.client.close();\n+\n+                            // further delaying the emit of 'expired' event seems to avoid\n+                            // intermittent errors thrown by node-zookeeper-client\n+                            setTimeout(() => {\n+                                this.client.emit(\"expired\");\n+                            }, 1000);\n+                        }\n+                        , this.sessionTimeout);\n+                });\n+                this.client.on('state', (state : Object) => {\n+                    this.emit(LocatorException.CODE.STATE_CHANGE, state, this.client.getSessionId());\n+                });\n+                this.client.on(\"expired\", () => {\n+                    this.emit(LocatorException.CODE.EXPIRED);\n+                    this.connect();\n+                });\n+\n+                this.emit(LocatorException.CODE.CONNECTING);\n+                this.client.connect();\n+            })\n+            .catch((err) => {\n+                this.emit(LocatorException.CODE.ZK_LOCATOR_ERROR, err);\n+                this.connect();\n+            });\n     }\n+}\n \n-    var promises = children.map(function (child) {\n-      var deferred = Promise.defer<Location>();\n+/**\n+ * ZookeeperPathLocatorFactory provides Locators for a list of hosts stored in a path in a zookeeper cluster\n+ */\n+class ZookeeperPathLocatorFactory {\n+    private clientWrapper : ZookeeperClientWrapper;\n+    private path : string;\n+    private emitter : EventEmitter;\n+    private dataExtractor : DataExtractor;\n+    private locatorTimeout : number;\n+    strict : boolean;\n+    next : number;\n+    locationPool : Location[];\n+    cachedLocationPool : Location[];\n+    locationRequestQueue : Promise.Resolver<Location>[];\n+    stale : boolean;\n+\n+    /**\n+     * zookeeper.client.getChildren response handler.  upon fetching the path we get a list of children nodes,\n+     * call getInfo on them, and store them in the location pool and kick of the location request processing queue\n+     * @param err\n+     * @param children\n+     */\n+    private onGetLocations = (err : Error, children : string[]) => {\n+        if (err) {\n+            this.emitter.emit(LocatorException.CODE.FAILED_TO_GET_CHILDREN, this.path, err);\n+            this.setLocationPool([]);\n+            this.processQueue();\n+            return;\n+        }\n+\n+        const promises = children.map((child) => {\n+            const locator = deferredLocationRequest();\n \n-      clientWrapper.client.getData(path + \"/\" + child, (_err, data) => {\n-        if (_err) {\n-          if (_err.getCode() !== Exception.NO_NODE) {\n-            emitter.emit(LocatorException.CODE[\"FAILED_TO_GET_CHILD_INFO\"], path, _err);\n-          }\n+            this.clientWrapper.client.getData(this.path + \"/\" + child, (_err, data) => {\n+                if (_err) {\n+                    if (_err.getCode() !== zookeeper.Exception.NO_NODE) {\n+                        this.emitter.emit(LocatorException.CODE.FAILED_TO_GET_CHILD_INFO, this.path, _err);","path":"src/zookeeper.ts","position":null,"original_position":348,"commit_id":"dd1c3a24a5de79d5f66373b1d561e5e6c62d45e6","original_commit_id":"1a03c566ef137f4e165c166cba698eec3dc0026d","user":{"login":"clintropolis","id":1577461,"avatar_url":"https://avatars.githubusercontent.com/u/1577461?v=3","gravatar_id":"","url":"https://api.github.com/users/clintropolis","html_url":"https://github.com/clintropolis","followers_url":"https://api.github.com/users/clintropolis/followers","following_url":"https://api.github.com/users/clintropolis/following{/other_user}","gists_url":"https://api.github.com/users/clintropolis/gists{/gist_id}","starred_url":"https://api.github.com/users/clintropolis/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/clintropolis/subscriptions","organizations_url":"https://api.github.com/users/clintropolis/orgs","repos_url":"https://api.github.com/users/clintropolis/repos","events_url":"https://api.github.com/users/clintropolis/events{/privacy}","received_events_url":"https://api.github.com/users/clintropolis/received_events","type":"User","site_admin":false},"body":"I'm not entirely sure why we resolve null here instead of rejecting, like we do in other places for errors, I was just following legacy behavior. I'll do some experiments and see if it breaks anything.","created_at":"2016-08-24T05:39:48Z","updated_at":"2016-08-24T05:39:48Z","html_url":"https://github.com/metamx/locators/pull/9#discussion_r75997345","pull_request_url":"https://api.github.com/repos/metamx/locators/pulls/9","_links":{"self":{"href":"https://api.github.com/repos/metamx/locators/pulls/comments/75997345"},"html":{"href":"https://github.com/metamx/locators/pull/9#discussion_r75997345"},"pull_request":{"href":"https://api.github.com/repos/metamx/locators/pulls/9"}}},"pull_request":{"url":"https://api.github.com/repos/metamx/locators/pulls/9","id":82347980,"html_url":"https://github.com/metamx/locators/pull/9","diff_url":"https://github.com/metamx/locators/pull/9.diff","patch_url":"https://github.com/metamx/locators/pull/9.patch","issue_url":"https://api.github.com/repos/metamx/locators/issues/9","number":9,"state":"open","locked":false,"title":"major refactor","user":{"login":"clintropolis","id":1577461,"avatar_url":"https://avatars.githubusercontent.com/u/1577461?v=3","gravatar_id":"","url":"https://api.github.com/users/clintropolis","html_url":"https://github.com/clintropolis","followers_url":"https://api.github.com/users/clintropolis/followers","following_url":"https://api.github.com/users/clintropolis/following{/other_user}","gists_url":"https://api.github.com/users/clintropolis/gists{/gist_id}","starred_url":"https://api.github.com/users/clintropolis/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/clintropolis/subscriptions","organizations_url":"https://api.github.com/users/clintropolis/orgs","repos_url":"https://api.github.com/users/clintropolis/repos","events_url":"https://api.github.com/users/clintropolis/events{/privacy}","received_events_url":"https://api.github.com/users/clintropolis/received_events","type":"User","site_admin":false},"body":"changes:\r\n* code has been significantly refactored and should (hopefully) be much easier to follow\r\n* updated readme\r\n* update typescript, use tsconfig.json instead of config in gruntfile\r\n* migrate from tsd to typings\r\n* migrate from q to bluebird\r\n* clean up some odds and ends","created_at":"2016-08-23T11:32:04Z","updated_at":"2016-08-24T05:39:48Z","closed_at":null,"merged_at":null,"merge_commit_sha":"090f56a8af817c96d6b6be805a3cf80dc588eed9","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/metamx/locators/pulls/9/commits","review_comments_url":"https://api.github.com/repos/metamx/locators/pulls/9/comments","review_comment_url":"https://api.github.com/repos/metamx/locators/pulls/comments{/number}","comments_url":"https://api.github.com/repos/metamx/locators/issues/9/comments","statuses_url":"https://api.github.com/repos/metamx/locators/statuses/dd1c3a24a5de79d5f66373b1d561e5e6c62d45e6","head":{"label":"metamx:refactor","ref":"refactor","sha":"dd1c3a24a5de79d5f66373b1d561e5e6c62d45e6","user":{"login":"metamx","id":299329,"avatar_url":"https://avatars.githubusercontent.com/u/299329?v=3","gravatar_id":"","url":"https://api.github.com/users/metamx","html_url":"https://github.com/metamx","followers_url":"https://api.github.com/users/metamx/followers","following_url":"https://api.github.com/users/metamx/following{/other_user}","gists_url":"https://api.github.com/users/metamx/gists{/gist_id}","starred_url":"https://api.github.com/users/metamx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/metamx/subscriptions","organizations_url":"https://api.github.com/users/metamx/orgs","repos_url":"https://api.github.com/users/metamx/repos","events_url":"https://api.github.com/users/metamx/events{/privacy}","received_events_url":"https://api.github.com/users/metamx/received_events","type":"Organization","site_admin":false},"repo":{"id":28058798,"name":"locators","full_name":"metamx/locators","owner":{"login":"metamx","id":299329,"avatar_url":"https://avatars.githubusercontent.com/u/299329?v=3","gravatar_id":"","url":"https://api.github.com/users/metamx","html_url":"https://github.com/metamx","followers_url":"https://api.github.com/users/metamx/followers","following_url":"https://api.github.com/users/metamx/following{/other_user}","gists_url":"https://api.github.com/users/metamx/gists{/gist_id}","starred_url":"https://api.github.com/users/metamx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/metamx/subscriptions","organizations_url":"https://api.github.com/users/metamx/orgs","repos_url":"https://api.github.com/users/metamx/repos","events_url":"https://api.github.com/users/metamx/events{/privacy}","received_events_url":"https://api.github.com/users/metamx/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/metamx/locators","description":"A collection of service locators using different mechanisms","fork":false,"url":"https://api.github.com/repos/metamx/locators","forks_url":"https://api.github.com/repos/metamx/locators/forks","keys_url":"https://api.github.com/repos/metamx/locators/keys{/key_id}","collaborators_url":"https://api.github.com/repos/metamx/locators/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/metamx/locators/teams","hooks_url":"https://api.github.com/repos/metamx/locators/hooks","issue_events_url":"https://api.github.com/repos/metamx/locators/issues/events{/number}","events_url":"https://api.github.com/repos/metamx/locators/events","assignees_url":"https://api.github.com/repos/metamx/locators/assignees{/user}","branches_url":"https://api.github.com/repos/metamx/locators/branches{/branch}","tags_url":"https://api.github.com/repos/metamx/locators/tags","blobs_url":"https://api.github.com/repos/metamx/locators/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/metamx/locators/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/metamx/locators/git/refs{/sha}","trees_url":"https://api.github.com/repos/metamx/locators/git/trees{/sha}","statuses_url":"https://api.github.com/repos/metamx/locators/statuses/{sha}","languages_url":"https://api.github.com/repos/metamx/locators/languages","stargazers_url":"https://api.github.com/repos/metamx/locators/stargazers","contributors_url":"https://api.github.com/repos/metamx/locators/contributors","subscribers_url":"https://api.github.com/repos/metamx/locators/subscribers","subscription_url":"https://api.github.com/repos/metamx/locators/subscription","commits_url":"https://api.github.com/repos/metamx/locators/commits{/sha}","git_commits_url":"https://api.github.com/repos/metamx/locators/git/commits{/sha}","comments_url":"https://api.github.com/repos/metamx/locators/comments{/number}","issue_comment_url":"https://api.github.com/repos/metamx/locators/issues/comments{/number}","contents_url":"https://api.github.com/repos/metamx/locators/contents/{+path}","compare_url":"https://api.github.com/repos/metamx/locators/compare/{base}...{head}","merges_url":"https://api.github.com/repos/metamx/locators/merges","archive_url":"https://api.github.com/repos/metamx/locators/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/metamx/locators/downloads","issues_url":"https://api.github.com/repos/metamx/locators/issues{/number}","pulls_url":"https://api.github.com/repos/metamx/locators/pulls{/number}","milestones_url":"https://api.github.com/repos/metamx/locators/milestones{/number}","notifications_url":"https://api.github.com/repos/metamx/locators/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/metamx/locators/labels{/name}","releases_url":"https://api.github.com/repos/metamx/locators/releases{/id}","deployments_url":"https://api.github.com/repos/metamx/locators/deployments","created_at":"2014-12-15T22:14:43Z","updated_at":"2015-04-24T01:07:16Z","pushed_at":"2016-08-24T05:21:53Z","git_url":"git://github.com/metamx/locators.git","ssh_url":"git@github.com:metamx/locators.git","clone_url":"https://github.com/metamx/locators.git","svn_url":"https://github.com/metamx/locators","homepage":null,"size":77,"stargazers_count":0,"watchers_count":0,"language":"CoffeeScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"metamx:master","ref":"master","sha":"c4d694ddac902a9cd9a5681f3f20393910ef58e1","user":{"login":"metamx","id":299329,"avatar_url":"https://avatars.githubusercontent.com/u/299329?v=3","gravatar_id":"","url":"https://api.github.com/users/metamx","html_url":"https://github.com/metamx","followers_url":"https://api.github.com/users/metamx/followers","following_url":"https://api.github.com/users/metamx/following{/other_user}","gists_url":"https://api.github.com/users/metamx/gists{/gist_id}","starred_url":"https://api.github.com/users/metamx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/metamx/subscriptions","organizations_url":"https://api.github.com/users/metamx/orgs","repos_url":"https://api.github.com/users/metamx/repos","events_url":"https://api.github.com/users/metamx/events{/privacy}","received_events_url":"https://api.github.com/users/metamx/received_events","type":"Organization","site_admin":false},"repo":{"id":28058798,"name":"locators","full_name":"metamx/locators","owner":{"login":"metamx","id":299329,"avatar_url":"https://avatars.githubusercontent.com/u/299329?v=3","gravatar_id":"","url":"https://api.github.com/users/metamx","html_url":"https://github.com/metamx","followers_url":"https://api.github.com/users/metamx/followers","following_url":"https://api.github.com/users/metamx/following{/other_user}","gists_url":"https://api.github.com/users/metamx/gists{/gist_id}","starred_url":"https://api.github.com/users/metamx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/metamx/subscriptions","organizations_url":"https://api.github.com/users/metamx/orgs","repos_url":"https://api.github.com/users/metamx/repos","events_url":"https://api.github.com/users/metamx/events{/privacy}","received_events_url":"https://api.github.com/users/metamx/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/metamx/locators","description":"A collection of service locators using different mechanisms","fork":false,"url":"https://api.github.com/repos/metamx/locators","forks_url":"https://api.github.com/repos/metamx/locators/forks","keys_url":"https://api.github.com/repos/metamx/locators/keys{/key_id}","collaborators_url":"https://api.github.com/repos/metamx/locators/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/metamx/locators/teams","hooks_url":"https://api.github.com/repos/metamx/locators/hooks","issue_events_url":"https://api.github.com/repos/metamx/locators/issues/events{/number}","events_url":"https://api.github.com/repos/metamx/locators/events","assignees_url":"https://api.github.com/repos/metamx/locators/assignees{/user}","branches_url":"https://api.github.com/repos/metamx/locators/branches{/branch}","tags_url":"https://api.github.com/repos/metamx/locators/tags","blobs_url":"https://api.github.com/repos/metamx/locators/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/metamx/locators/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/metamx/locators/git/refs{/sha}","trees_url":"https://api.github.com/repos/metamx/locators/git/trees{/sha}","statuses_url":"https://api.github.com/repos/metamx/locators/statuses/{sha}","languages_url":"https://api.github.com/repos/metamx/locators/languages","stargazers_url":"https://api.github.com/repos/metamx/locators/stargazers","contributors_url":"https://api.github.com/repos/metamx/locators/contributors","subscribers_url":"https://api.github.com/repos/metamx/locators/subscribers","subscription_url":"https://api.github.com/repos/metamx/locators/subscription","commits_url":"https://api.github.com/repos/metamx/locators/commits{/sha}","git_commits_url":"https://api.github.com/repos/metamx/locators/git/commits{/sha}","comments_url":"https://api.github.com/repos/metamx/locators/comments{/number}","issue_comment_url":"https://api.github.com/repos/metamx/locators/issues/comments{/number}","contents_url":"https://api.github.com/repos/metamx/locators/contents/{+path}","compare_url":"https://api.github.com/repos/metamx/locators/compare/{base}...{head}","merges_url":"https://api.github.com/repos/metamx/locators/merges","archive_url":"https://api.github.com/repos/metamx/locators/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/metamx/locators/downloads","issues_url":"https://api.github.com/repos/metamx/locators/issues{/number}","pulls_url":"https://api.github.com/repos/metamx/locators/pulls{/number}","milestones_url":"https://api.github.com/repos/metamx/locators/milestones{/number}","notifications_url":"https://api.github.com/repos/metamx/locators/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/metamx/locators/labels{/name}","releases_url":"https://api.github.com/repos/metamx/locators/releases{/id}","deployments_url":"https://api.github.com/repos/metamx/locators/deployments","created_at":"2014-12-15T22:14:43Z","updated_at":"2015-04-24T01:07:16Z","pushed_at":"2016-08-24T05:21:53Z","git_url":"git://github.com/metamx/locators.git","ssh_url":"git@github.com:metamx/locators.git","clone_url":"https://github.com/metamx/locators.git","svn_url":"https://github.com/metamx/locators","homepage":null,"size":77,"stargazers_count":0,"watchers_count":0,"language":"CoffeeScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/metamx/locators/pulls/9"},"html":{"href":"https://github.com/metamx/locators/pull/9"},"issue":{"href":"https://api.github.com/repos/metamx/locators/issues/9"},"comments":{"href":"https://api.github.com/repos/metamx/locators/issues/9/comments"},"review_comments":{"href":"https://api.github.com/repos/metamx/locators/pulls/9/comments"},"review_comment":{"href":"https://api.github.com/repos/metamx/locators/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/metamx/locators/pulls/9/commits"},"statuses":{"href":"https://api.github.com/repos/metamx/locators/statuses/dd1c3a24a5de79d5f66373b1d561e5e6c62d45e6"}}}}
{"actor":{"display_login":"clintropolis"}}