function TernModule() {
        var ternPromise         = null,
            addFilesPromise     = null,
            rootTernDir         = null,
            projectRoot         = null,
            stopAddingFiles     = false,
            resolvedFiles       = {},       // file -> resolved file
            numInitialFiles     = 0,
            numResolvedFiles    = 0,
            numAddedFiles       = 0,
            _ternNodeDomain     = null;

        /**
         * @param {string} file a relative path
         * @return {string} returns the path we resolved when we tried to parse the file, or undefined
         */
        function getResolvedPath(file) {
            return resolvedFiles[file];
        }

        /**
         *  Determine whether the current set of files are using modules to pull in
         *  additional files.
         *
         * @return {boolean} - true if more files than the current directory have
         * been read in.
         */
        function usingModules() {
            return numInitialFiles !== numResolvedFiles;
        }

        /**
         * Send a message to the tern node domain - if the module is being initialized,
         * the message will not be posted until initialization is complete
         */
        function postMessage(msg) {
            addFilesPromise.done(function (ternModule) {
                // If an error came up during file handling, bail out now
                if (!_ternNodeDomain) {
                    return;
                }

                if (config.debug) {
                    console.debug("Sending message", msg);
                }
                _ternNodeDomain.exec("invokeTernCommand", msg);
            });
        }

        /**
         * Send a message to the tern node domain - this is only for messages that
         * need to be sent before and while the addFilesPromise is being resolved.
         */
        function _postMessageByPass(msg) {
            ternPromise.done(function (ternModule) {
                if (config.debug) {
                    console.debug("Sending message", msg);
                }
                _ternNodeDomain.exec("invokeTernCommand", msg);
            });
        }

        /**
         *  Update tern with the new contents of a given file.
         *
         * @param {Document} document - the document to update
         * @return {jQuery.Promise} - the promise for the request
         */
        function updateTernFile(document) {
            var path  = document.file.fullPath;

            _postMessageByPass({
                type       : MessageIds.TERN_UPDATE_FILE_MSG,
                path       : path,
                text       : getTextFromDocument(document)
            });

            return addPendingRequest(path, OFFSET_ZERO, MessageIds.TERN_UPDATE_FILE_MSG);
        }

        /**
         * Handle a request from the tern node domain for text of a file
         *
         * @param {{file:string}} request - the request from the tern node domain.  Should be an Object containing the name
         *      of the file tern wants the contents of 
         */
        function handleTernGetFile(request) {

            function replyWith(name, txt) {
                _postMessageByPass({
                    type: MessageIds.TERN_GET_FILE_MSG,
                    file: name,
                    text: txt
                });
            }

            var name = request.file;

            /**
             * Helper function to get the text of a given document and send it to tern.
             * If DocumentManager successfully gets the file's text then we'll send it to the tern node domain.
             * The Promise for getDocumentText() is returned so that custom fail functions can be used.
             *
             * @param {string} filePath - the path of the file to get the text of
             * @return {jQuery.Promise} - the Promise returned from DocumentMangaer.getDocumentText()
             */
            function getDocText(filePath) {
                if (!FileSystem.isAbsolutePath(filePath) || // don't handle URLs
                        filePath.slice(0, 2) === "//") { // don't handle protocol-relative URLs like //example.com/main.js (see #10566)
                    return (new $.Deferred()).reject().promise();
                }

                var file = FileSystem.getFileForPath(filePath),
                    promise = DocumentManager.getDocumentText(file);

                promise.done(function (docText) {
                    resolvedFiles[name] = filePath;
                    numResolvedFiles++;
                    replyWith(name, filterText(docText));
                });
                return promise;
            }

            /**
             * Helper function to find any files in the project that end with the
             * name we are looking for.  This is so we can find requirejs modules
             * when the baseUrl is unknown, or when the project root is not the same
             * as the script root (e.g. if you open the 'brackets' dir instead of 'brackets/src' dir).
             */
            function findNameInProject() {
                // check for any files in project that end with the right path.
                var fileName = name.substring(name.lastIndexOf("/") + 1);

                function _fileFilter(entry) {
                    return entry.name === fileName;
                }

                ProjectManager.getAllFiles(_fileFilter).done(function (files) {
                    var file;
                    files = files.filter(function (file) {
                        var pos = file.fullPath.length - name.length;
                        return pos === file.fullPath.lastIndexOf(name);
                    });

                    if (files.length === 1) {
                        file = files[0];
                    }
                    if (file) {
                        getDocText(file.fullPath).fail(function () {
                            replyWith(name, "");
                        });
                    } else {
                        replyWith(name, "");
                    }
                });
            }

            if (!isFileExcludedInternal(name)) {
                getDocText(name).fail(function () {
                    getDocText(rootTernDir + name).fail(function () {
                        // check relative to project root
                        getDocText(projectRoot + name)
                            // last look for any files that end with the right path
                            // in the project
                            .fail(findNameInProject);
                    });
                });
            }
        }

        /**
         *  Prime the pump for a fast first lookup.
         *
         * @param {string} path - full path of file
         * @return {jQuery.Promise} - the promise for the request
         */
        function primePump(path, isUntitledDoc) {
            _postMessageByPass({
                type            : MessageIds.TERN_PRIME_PUMP_MSG,
                path            : path,
                isUntitledDoc   : isUntitledDoc
            });

            return addPendingRequest(path, OFFSET_ZERO, MessageIds.TERN_PRIME_PUMP_MSG);
        }

        /**
         * Handle the response from the tern node domain when
         * it responds to the prime pump message.
         *
         * @param {{path: string, type: string}} response - the response from node domain
         */
        function handlePrimePumpCompletion(response) {

            var path = response.path,
                type = response.type,
                $deferredHints = getPendingRequest(path, OFFSET_ZERO, type);

            if ($deferredHints) {
                $deferredHints.resolve();
            }
        }

        /**
         *  Add new files to tern, keeping any previous files.
         *  The tern server must be initialized before making
         *  this call.
         *
         * @param {Array.<string>} files - array of file to add to tern.
         * @return {boolean} - true if more files may be added, false if maximum has been reached.
         */
        function addFilesToTern(files) {
            // limit the number of files added to tern.
            var maxFileCount = preferences.getMaxFileCount();
            if (numResolvedFiles + numAddedFiles < maxFileCount) {
                var available = maxFileCount - numResolvedFiles - numAddedFiles;

                if (available < files.length) {
                    files = files.slice(0, available);
                }

                numAddedFiles += files.length;
                ternPromise.done(function (ternModule) {
                    var msg = {
                        type        : MessageIds.TERN_ADD_FILES_MSG,
                        files       : files
                    };

                    if (config.debug) {
                        console.debug("Sending message", msg);
                    }
                    _ternNodeDomain.exec("invokeTernCommand", msg);
                });

            } else {
                stopAddingFiles = true;
            }

            return stopAddingFiles;
        }

        /**
         *  Add the files in the directory and subdirectories of a given directory
         *  to tern.
         *
         * @param {string} dir - the root directory to add.
         * @param {function ()} doneCallback - called when all files have been
         * added to tern.
         */
        function addAllFilesAndSubdirectories(dir, doneCallback) {
            FileSystem.resolve(dir, function (err, directory) {
                function visitor(entry) {
                    if (entry.isFile) {
                        if (!isFileExcluded(entry)) { // ignore .dotfiles and non-.js files
                            addFilesToTern([entry.fullPath]);
                        }
                    } else {
                        return !isDirectoryExcluded(entry.fullPath) &&
                            entry.name.indexOf(".") !== 0 &&
                            !stopAddingFiles;
                    }
                }

                if (err) {
                    return;
                }

                if (dir === FileSystem.getDirectoryForPath(rootTernDir)) {
                    doneCallback();
                    return;
                }

                directory.visit(visitor, doneCallback);
            });
        }

        /**
         * Init the Tern module that does all the code hinting work.
         */
        function initTernModule() {
            var moduleDeferred = $.Deferred();
            ternPromise = moduleDeferred.promise();
            
            function prepareTern() {  
                _ternNodeDomain.exec("setInterface", {
                    messageIds : MessageIds
                });

                _ternNodeDomain.exec("invokeTernCommand", {
                    type: MessageIds.SET_CONFIG,
                    config: config
                });
                moduleDeferred.resolveWith(null, [_ternNodeDomain]);
            }
            
            if (_ternNodeDomain) {
                _ternNodeDomain.exec("resetTernServer");
                moduleDeferred.resolveWith(null, [_ternNodeDomain]);
            } else {
                _ternNodeDomain     = new NodeDomain("TernNodeDomain", _domainPath);
                _ternNodeDomain.on("data", function (evt, data) {
                    if (config.debug) {
                        console.log("Message received", data.type);
                    }

                    var response = data,
                        type = response.type;

                    if (type === MessageIds.TERN_COMPLETIONS_MSG ||
                            type === MessageIds.TERN_CALLED_FUNC_TYPE_MSG) {
                        // handle any completions the tern server calculated
                        handleTernCompletions(response);
                    } else if (type === MessageIds.TERN_GET_FILE_MSG) {
                        // handle a request for the contents of a file
                        handleTernGetFile(response);
                    } else if (type === MessageIds.TERN_JUMPTODEF_MSG) {
                        handleJumptoDef(response);
                    } else if (type === MessageIds.TERN_SCOPEDATA_MSG) {
                        handleScopeData(response);
                    } else if (type === MessageIds.TERN_REFS) {
                        handleRename(response);
                    } else if (type === MessageIds.TERN_PRIME_PUMP_MSG) {
                        handlePrimePumpCompletion(response);
                    } else if (type === MessageIds.TERN_GET_GUESSES_MSG) {
                        handleGetGuesses(response);
                    } else if (type === MessageIds.TERN_UPDATE_FILE_MSG) {
                        handleUpdateFile(response);
                    } else if (type === MessageIds.TERN_INFERENCE_TIMEDOUT) {
                        handleTimedOut(response);
                    } else if (type === MessageIds.TERN_WORKER_READY) {
                        moduleDeferred.resolveWith(null, [_ternNodeDomain]);
                    } else if (type === "RE_INIT_TERN") {
                        // Ensure the request is because of a node restart
                        if (currentModule) {
                            prepareTern();
                            // Mark the module with resetForced, then creation of TernModule will 
                            // happen again as part of '_maybeReset' call
                            currentModule.resetForced = true;
                        }
                    } else {
                        console.log("Tern Module: " + (response.log || response));
                    }
                });
            
                _ternNodeDomain.promise().done(prepareTern);
            }
        }

        /**
         * Create a new tern server.
         */
        function initTernServer(dir, files) {
            initTernModule();
            numResolvedFiles = 0;
            numAddedFiles = 0;
            stopAddingFiles = false;
            numInitialFiles = files.length;

            ternPromise.done(function (ternModule) {
                var msg = {
                    type        : MessageIds.TERN_INIT_MSG,
                    dir         : dir,
                    files       : files,
                    env         : ternEnvironment,
                    timeout     : PreferencesManager.get("jscodehints.inferenceTimeout")
                };
                _ternNodeDomain.exec("invokeTernCommand", msg);
            });
            rootTernDir = dir + "/";
        }

        /**
         *  We can skip tern initialization if we are opening a file that has
         *  already been added to tern.
         *
         * @param {string} newFile - full path of new file being opened in the editor.
         * @return {boolean} - true if tern initialization should be skipped,
         * false otherwise.
         */
        function canSkipTernInitialization(newFile) {
            return resolvedFiles[newFile] !== undefined;
        }


        /**
         *  Do the work to initialize a code hinting session.
         *
         * @param {Session} session - the active hinting session (TODO: currently unused)
         * @param {!Document} document - the document the editor has changed to
         * @param {?Document} previousDocument - the document the editor has changed from
         */
        function doEditorChange(session, document, previousDocument) {
            var file        = document.file,
                path        = file.fullPath,
                dir         = file.parentPath,
                pr;

            var addFilesDeferred = $.Deferred();

            documentChanges = null;
            addFilesPromise = addFilesDeferred.promise();
            pr = ProjectManager.getProjectRoot() ? ProjectManager.getProjectRoot().fullPath : null;

            // avoid re-initializing tern if possible.
            if (canSkipTernInitialization(path)) {

                // update the previous document in tern to prevent stale files.
                if (isDocumentDirty && previousDocument) {
                    var updateFilePromise = updateTernFile(previousDocument);
                    updateFilePromise.done(function () {
                        primePump(path, document.isUntitled());
                        addFilesDeferred.resolveWith(null, [_ternNodeDomain]);
                    });
                } else {
                    addFilesDeferred.resolveWith(null, [_ternNodeDomain]);
                }

                isDocumentDirty = false;
                return;
            }
            
            if (previousDocument && previousDocument.isDirty) {
                updateTernFile(previousDocument);
            }
    
            isDocumentDirty = false;
            resolvedFiles = {};
            projectRoot = pr;

            ensurePreferences();
            deferredPreferences.done(function () {
                if (file instanceof InMemoryFile) {
                    initTernServer(pr, []);
                    var hintsPromise = primePump(path, true);
                    hintsPromise.done(function () {
                        addFilesDeferred.resolveWith(null, [_ternNodeDomain]);
                    });
                    return;
                }

                FileSystem.resolve(dir, function (err, directory) {
                    if (err) {
                        console.error("Error resolving", dir);
                        addFilesDeferred.resolveWith(null);
                        return;
                    }

                    directory.getContents(function (err, contents) {
                        if (err) {
                            console.error("Error getting contents for", directory);
                            addFilesDeferred.resolveWith(null);
                            return;
                        }

                        var files = contents
                            .filter(function (entry) {
                                return entry.isFile && !isFileExcluded(entry);
                            })
                            .map(function (entry) {
                                return entry.fullPath;
                            });

                        initTernServer(dir, files);

                        var hintsPromise = primePump(path, false);
                        hintsPromise.done(function () {
                            if (!usingModules()) {
                                // Read the subdirectories of the new file's directory.
                                // Read them first in case there are too many files to
                                // read in the project.
                                addAllFilesAndSubdirectories(dir, function () {
                                    // If the file is in the project root, then read
                                    // all the files under the project root.
                                    var currentDir = (dir + "/");
                                    if (projectRoot && currentDir !== projectRoot &&
                                            currentDir.indexOf(projectRoot) === 0) {
                                        addAllFilesAndSubdirectories(projectRoot, function () {
                                            // prime the pump again but this time don't wait
                                            // for completion.
                                            primePump(path, false);
                                            addFilesDeferred.resolveWith(null, [_ternNodeDomain]);
                                        });
                                    } else {
                                        addFilesDeferred.resolveWith(null, [_ternNodeDomain]);
                                    }
                                });
                            } else {
                                addFilesDeferred.resolveWith(null, [_ternNodeDomain]);
                            }
                        });
                    });
                });
            });
        }

        /**
         * Called each time a new editor becomes active.
         *
         * @param {Session} session - the active hinting session (TODO: currently unused by doEditorChange())
         * @param {!Document} document - the document of the editor that has changed
         * @param {?Document} previousDocument - the document of the editor is changing from
         */
        function handleEditorChange(session, document, previousDocument) {
            if (addFilesPromise === null) {
                doEditorChange(session, document, previousDocument);
            } else {
                addFilesPromise.done(function () {
                    doEditorChange(session, document, previousDocument);
                });
            }
        }

        /**
         * Do some cleanup when a project is closed.
         *
         * We can clean up the node tern server we use to calculate hints now, since
         * we know we will need to re-init it in any new project that is opened.  
         */
        function resetModule() {
            function resetTernServer() {
                if (_ternNodeDomain.ready()) {
                    _ternNodeDomain.exec('resetTernServer');
                }
            }
            
            if (_ternNodeDomain) {
                if (addFilesPromise) {
                    // If we're in the middle of added files, don't reset 
                    // until we're done
                    addFilesPromise.done(resetTernServer).fail(resetTernServer);
                } else {
                    resetTernServer();
                }
            }
        }

        function whenReady(func) {
            addFilesPromise.done(func);
        }
        
        this.resetModule = resetModule;
        this.handleEditorChange = handleEditorChange;
        this.postMessage = postMessage;
        this.getResolvedPath = getResolvedPath;
        this.whenReady = whenReady;

        return this;
    }
Encapsulate all the logic to talk to the tern module.  This will create
a new instance of a TernModule, which the rest of the hinting code can use to talk
to the tern node domain, without worrying about initialization, priming the pump, etc.