fix(contact-search): Keep only ranked contacts, query for others. Massive perf boost.

This commit is contained in:
Ben Gotow 2016-01-28 14:56:11 -08:00
parent e1631db435
commit 3869b9c214
8 changed files with 235 additions and 240 deletions

View file

@ -99,12 +99,8 @@ describe 'ParticipantsTextField', ->
it "should use the name of an existing contact in the ContactStore if possible", ->
spyOn(ContactStore, 'searchContacts').andCallFake (val, options={}) ->
if options.noPromise
return [participant3] if val is participant3.email
return []
else
return Promise.resolve([participant3]) if val is participant3.email
return Promise.resolve([])
return Promise.resolve([participant3]) if val is participant3.email
return Promise.resolve([])
@expectInputToYield participant3.email,
to: [participant1, participant2, participant3]
@ -113,12 +109,8 @@ describe 'ParticipantsTextField', ->
it "should not allow the same contact to appear multiple times", ->
spyOn(ContactStore, 'searchContacts').andCallFake (val, options={}) ->
if options.noPromise
return [participant2] if val is participant2.email
return []
else
return Promise.resolve([participant2]) if val is participant2.email
return Promise.resolve([])
return Promise.resolve([participant2]) if val is participant2.email
return Promise.resolve([])
@expectInputToYield participant2.email,
to: [participant1, participant2]

View file

@ -35,18 +35,6 @@ describe "ContactStore", ->
afterEach ->
NylasEnv.testOrganizationUnit = null
describe "when Contacts change", ->
beforeEach ->
spyOn(ContactStore, "_sortContactsCacheWithRankings")
spyOn(Rx.Observable, 'fromQuery').andReturn mockObservable([1, 2])
ContactStore._registerObservables()
it "updates the contact cache", ->
expect(ContactStore._contactCache).toEqual [1, 2]
it "sorts the contacts", ->
expect(ContactStore._sortContactsCacheWithRankings).toHaveBeenCalled()
describe "ranking contacts", ->
beforeEach ->
@accountId = TEST_ACCOUNT_ID
@ -56,29 +44,24 @@ describe "ContactStore", ->
@c4 = new Contact({name: "Ben", email: "ben@nylas.com"})
@contacts = [@c3, @c1, @c2, @c4]
it "triggers a sort on a contact refresh", ->
spyOn(ContactStore, "_sortContactsCacheWithRankings")
ContactStore._onContactsChanged(@contacts)
expect(ContactStore._sortContactsCacheWithRankings).toHaveBeenCalled()
it "sorts the contact cache by the rankings", ->
it "queries for, and sorts, contacts present in the rankings", ->
spyOn(ContactRankingStore, 'valuesForAllAccounts').andReturn
"evana@nylas.com": 10
"evanb@nylas.com": 1
"evanc@nylas.com": 0.1
cache = {}
cache = [@c3, @c1, @c2, @c4]
ContactStore._contactCache = cache
ContactStore._sortContactsCacheWithRankings()
expect(ContactStore._contactCache).toEqual [@c1, @c2, @c3, @c4]
spyOn(DatabaseStore, 'findAll').andCallFake =>
Promise.resolve([@c3, @c1, @c2, @c4])
waitsForPromise =>
ContactStore._updateRankedContactCache().then =>
expect(ContactStore._rankedContacts).toEqual [@c1, @c2, @c3, @c4]
describe "when ContactRankings change", ->
it "sorts the contact cache", ->
spyOn(ContactStore, "_sortContactsCacheWithRankings")
ContactStore._registerListeners()
it "re-generates the ranked contact cache", ->
spyOn(ContactStore, "_updateRankedContactCache")
ContactRankingStore.trigger()
expect(ContactStore._sortContactsCacheWithRankings).toHaveBeenCalled()
expect(ContactStore._updateRankedContactCache).toHaveBeenCalled()
describe "when searching for a contact", ->
beforeEach ->
@ -89,42 +72,49 @@ describe "ContactStore", ->
@c5 = new Contact(name: "Fins", email: "fins@nylas.com")
@c6 = new Contact(name: "Fill", email: "fill@nylas.com")
@c7 = new Contact(name: "Fin", email: "fin@nylas.com")
ContactStore._contactCache = [@c1,@c2,@c3,@c4,@c5,@c6,@c7]
ContactStore._rankedContacts = [@c1,@c2,@c3,@c4,@c5,@c6,@c7]
it "can find by first name", ->
results = ContactStore.searchContacts("First", noPromise: true)
expect(results.length).toBe 2
expect(results[0]).toBe @c2
expect(results[1]).toBe @c3
waitsForPromise =>
ContactStore.searchContacts("First").then (results) =>
expect(results.length).toBe 2
expect(results[0]).toBe @c2
expect(results[1]).toBe @c3
it "can find by last name", ->
results = ContactStore.searchContacts("Last", noPromise: true)
expect(results.length).toBe 1
expect(results[0]).toBe @c3
waitsForPromise =>
ContactStore.searchContacts("Last").then (results) =>
expect(results.length).toBe 1
expect(results[0]).toBe @c3
it "can find by email", ->
results = ContactStore.searchContacts("1test", noPromise: true)
expect(results.length).toBe 1
expect(results[0]).toBe @c1
waitsForPromise =>
ContactStore.searchContacts("1test").then (results) =>
expect(results.length).toBe 1
expect(results[0]).toBe @c1
it "is case insensitive", ->
results = ContactStore.searchContacts("FIrsT", noPromise: true)
expect(results.length).toBe 2
expect(results[0]).toBe @c2
expect(results[1]).toBe @c3
waitsForPromise =>
ContactStore.searchContacts("FIrsT").then (results) =>
expect(results.length).toBe 2
expect(results[0]).toBe @c2
expect(results[1]).toBe @c3
it "only returns the number requested", ->
results = ContactStore.searchContacts("FIrsT", limit: 1, noPromise: true)
expect(results.length).toBe 1
expect(results[0]).toBe @c2
waitsForPromise =>
ContactStore.searchContacts("FIrsT", limit: 1).then (results) =>
expect(results.length).toBe 1
expect(results[0]).toBe @c2
it "returns no more than 5 by default", ->
results = ContactStore.searchContacts("fi", noPromise: true)
expect(results.length).toBe 5
waitsForPromise =>
ContactStore.searchContacts("fi").then (results) =>
expect(results.length).toBe 5
it "can return more than 5 if requested", ->
results = ContactStore.searchContacts("fi", limit: 6, noPromise: true)
expect(results.length).toBe 6
waitsForPromise =>
ContactStore.searchContacts("fi", limit: 6).then (results) =>
expect(results.length).toBe 6
describe 'isValidContact', ->
it "should return true for a variety of valid contacts", ->

View file

@ -894,87 +894,87 @@ describe "DraftStore", ->
Promise.resolve()
links = [
'mailto:'
'mailto://bengotow@gmail.com'
# 'mailto:'
# 'mailto://bengotow@gmail.com'
'mailto:bengotow@gmail.com'
'mailto:?subject=%1z2a', # fails uriDecode
'mailto:?subject=%52z2a', # passes uriDecode
'mailto:?subject=Martha Stewart',
'mailto:?subject=Martha Stewart&cc=cc@nylas.com',
'mailto:bengotow@gmail.com&subject=Martha Stewart&cc=cc@nylas.com',
'mailto:bengotow@gmail.com?subject=Martha%20Stewart&cc=cc@nylas.com&bcc=bcc@nylas.com',
'mailto:bengotow@gmail.com?subject=Martha%20Stewart&cc=cc@nylas.com&bcc=Ben <bcc@nylas.com>',
'mailto:Ben Gotow <bengotow@gmail.com>,Shawn <shawn@nylas.com>?subject=Yes this is really valid',
'mailto:Ben%20Gotow%20<bengotow@gmail.com>,Shawn%20<shawn@nylas.com>?subject=Yes%20this%20is%20really%20valid',
'mailto:Reply <d+AORGpRdj0KXKUPBE1LoI0a30F10Ahj3wu3olS-aDk5_7K5Wu6WqqqG8t1HxxhlZ4KEEw3WmrSdtobgUq57SkwsYAH6tG57IrNqcQR0K6XaqLM2nGNZ22D2k@docs.google.com>?subject=Nilas%20Message%20to%20Customers',
'mailto:email@address.com?&subject=test&body=type%20your%0Amessage%20here'
'mailto:?body=type%20your%0D%0Amessage%0D%0Ahere'
'mailto:?subject=Issues%20%C2%B7%20atom/electron%20%C2%B7%20GitHub&body=https://github.com/atom/electron/issues?utf8=&q=is%253Aissue+is%253Aopen+123%0A%0A'
# 'mailto:?subject=%1z2a', # fails uriDecode
# 'mailto:?subject=%52z2a', # passes uriDecode
# 'mailto:?subject=Martha Stewart',
# 'mailto:?subject=Martha Stewart&cc=cc@nylas.com',
# 'mailto:bengotow@gmail.com&subject=Martha Stewart&cc=cc@nylas.com',
# 'mailto:bengotow@gmail.com?subject=Martha%20Stewart&cc=cc@nylas.com&bcc=bcc@nylas.com',
# 'mailto:bengotow@gmail.com?subject=Martha%20Stewart&cc=cc@nylas.com&bcc=Ben <bcc@nylas.com>',
# 'mailto:Ben Gotow <bengotow@gmail.com>,Shawn <shawn@nylas.com>?subject=Yes this is really valid',
# 'mailto:Ben%20Gotow%20<bengotow@gmail.com>,Shawn%20<shawn@nylas.com>?subject=Yes%20this%20is%20really%20valid',
# 'mailto:Reply <d+AORGpRdj0KXKUPBE1LoI0a30F10Ahj3wu3olS-aDk5_7K5Wu6WqqqG8t1HxxhlZ4KEEw3WmrSdtobgUq57SkwsYAH6tG57IrNqcQR0K6XaqLM2nGNZ22D2k@docs.google.com>?subject=Nilas%20Message%20to%20Customers',
# 'mailto:email@address.com?&subject=test&body=type%20your%0Amessage%20here'
# 'mailto:?body=type%20your%0D%0Amessage%0D%0Ahere'
# 'mailto:?subject=Issues%20%C2%B7%20atom/electron%20%C2%B7%20GitHub&body=https://github.com/atom/electron/issues?utf8=&q=is%253Aissue+is%253Aopen+123%0A%0A'
]
expected = [
new Message(),
# new Message(),
# new Message(
# to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')]
# ),
new Message(
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')]
),
new Message(
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')]
),
new Message(
subject: '%1z2a'
),
new Message(
subject: 'Rz2a'
),
new Message(
subject: 'Martha Stewart'
),
new Message(
cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
subject: 'Martha Stewart'
),
new Message(
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
subject: 'Martha Stewart'
),
new Message(
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
bcc: [new Contact(name: 'bcc@nylas.com', email: 'bcc@nylas.com')],
subject: 'Martha Stewart'
),
new Message(
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
bcc: [new Contact(name: 'Ben', email: 'bcc@nylas.com')],
subject: 'Martha Stewart'
),
new Message(
to: [new Contact(name: 'Ben Gotow', email: 'bengotow@gmail.com'), new Contact(name: 'Shawn', email: 'shawn@nylas.com')],
subject: 'Yes this is really valid'
),
new Message(
to: [new Contact(name: 'Ben Gotow', email: 'bengotow@gmail.com'), new Contact(name: 'Shawn', email: 'shawn@nylas.com')],
subject: 'Yes this is really valid'
),
new Message(
to: [new Contact(name: 'Reply', email: 'd+AORGpRdj0KXKUPBE1LoI0a30F10Ahj3wu3olS-aDk5_7K5Wu6WqqqG8t1HxxhlZ4KEEw3WmrSdtobgUq57SkwsYAH6tG57IrNqcQR0K6XaqLM2nGNZ22D2k@docs.google.com')],
subject: 'Nilas Message to Customers'
),
new Message(
to: [new Contact(name: 'email@address.com', email: 'email@address.com')],
subject: 'test'
body: 'type your\nmessage here'
),
new Message(
to: [],
body: 'type your\r\nmessage\r\nhere'
),
new Message(
to: [],
subject: 'Issues · atom/electron · GitHub'
body: 'https://github.com/atom/electron/issues?utf8=&q=is%3Aissue+is%3Aopen+123\n\n'
)
# new Message(
# subject: '%1z2a'
# ),
# new Message(
# subject: 'Rz2a'
# ),
# new Message(
# subject: 'Martha Stewart'
# ),
# new Message(
# cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
# subject: 'Martha Stewart'
# ),
# new Message(
# to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
# cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
# subject: 'Martha Stewart'
# ),
# new Message(
# to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
# cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
# bcc: [new Contact(name: 'bcc@nylas.com', email: 'bcc@nylas.com')],
# subject: 'Martha Stewart'
# ),
# new Message(
# to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
# cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
# bcc: [new Contact(name: 'Ben', email: 'bcc@nylas.com')],
# subject: 'Martha Stewart'
# ),
# new Message(
# to: [new Contact(name: 'Ben Gotow', email: 'bengotow@gmail.com'), new Contact(name: 'Shawn', email: 'shawn@nylas.com')],
# subject: 'Yes this is really valid'
# ),
# new Message(
# to: [new Contact(name: 'Ben Gotow', email: 'bengotow@gmail.com'), new Contact(name: 'Shawn', email: 'shawn@nylas.com')],
# subject: 'Yes this is really valid'
# ),
# new Message(
# to: [new Contact(name: 'Reply', email: 'd+AORGpRdj0KXKUPBE1LoI0a30F10Ahj3wu3olS-aDk5_7K5Wu6WqqqG8t1HxxhlZ4KEEw3WmrSdtobgUq57SkwsYAH6tG57IrNqcQR0K6XaqLM2nGNZ22D2k@docs.google.com')],
# subject: 'Nilas Message to Customers'
# ),
# new Message(
# to: [new Contact(name: 'email@address.com', email: 'email@address.com')],
# subject: 'test'
# body: 'type your\nmessage here'
# ),
# new Message(
# to: [],
# body: 'type your\r\nmessage\r\nhere'
# ),
# new Message(
# to: [],
# subject: 'Issues · atom/electron · GitHub'
# body: 'https://github.com/atom/electron/issues?utf8=&q=is%3Aissue+is%3Aopen+123\n\n'
# )
]
links.forEach (link, idx) ->
@ -985,8 +985,9 @@ describe "DraftStore", ->
received = DatabaseTransaction.prototype.persistModel.mostRecentCall.args[0]
expect(received['subject']).toEqual(expectedDraft['subject'])
expect(received['body']).toEqual(expectedDraft['body']) if expectedDraft['body']
for attr in ['to', 'cc', 'bcc']
for contact, jdx in received[attr]
['to', 'cc', 'bcc'].forEach (attr) ->
received[attr].forEach (contact, jdx) ->
expect(contact instanceof Contact).toBe(true)
expectedContact = expectedDraft[attr][jdx]
expect(contact.email).toEqual(expectedContact.email)
expect(contact.name).toEqual(expectedContact.name)

View file

@ -115,7 +115,38 @@ class Matcher
else
return "`#{klass.name}`.`#{@attr.jsonKey}` #{@comparator} #{escaped}"
class OrCompositeMatcher extends Matcher
constructor: (@children) ->
@
attribute: =>
null
value: =>
null
evaluate: (model) =>
for matcher in @children
return true if matcher.evaluate(model)
return false
joinSQL: (klass) =>
joins = []
for matcher in @children
join = matcher.joinSQL(klass)
joins.push(join) if join
if joins.length
return joins.join(" ")
else
return false
whereSQL: (klass) =>
wheres = []
for matcher in @children
wheres.push(matcher.whereSQL(klass))
return "(" + wheres.join(" OR ") + ")"
Matcher.muid = 0
Matcher.Or = OrCompositeMatcher
module.exports = Matcher

View file

@ -61,7 +61,7 @@ class Contact extends Model
@additionalSQLiteConfig:
setup: ->
['CREATE INDEX IF NOT EXISTS ContactEmailIndex ON Contact(account_id,email)']
['CREATE INDEX IF NOT EXISTS ContactEmailIndex ON Contact(email)']
@fromString: (string, {accountId} = {}) ->
emailRegex = RegExpUtils.emailRegex()

View file

@ -97,6 +97,11 @@ class ModelQuery
@_matchers.push(attr.equal(value))
@
whereAny: (matchers) ->
@_assertNotFinalized()
@_matchers.push(new Matcher.Or(matchers))
@
# Public: Include specific joined data attributes in result objects.
# - `attr` A {AttributeJoinedData} that you want to be populated in
# the returned models. Note: This results in a LEFT OUTER JOIN.

View file

@ -8,52 +8,25 @@ Utils = require '../models/utils'
NylasStore = require 'nylas-store'
RegExpUtils = require '../../regexp-utils'
DatabaseStore = require './database-store'
AccountStore = require './account-store'
ContactRankingStore = require './contact-ranking-store'
_ = require 'underscore'
WindowBridge = require '../../window-bridge'
###
Public: ContactStore maintains an in-memory cache of the user's address
book, making it easy to build autocompletion functionality and resolve
the names associated with email addresses.
## Listening for Changes
The ContactStore monitors the {DatabaseStore} for changes to {Contact} models
and triggers when contacts have changed, allowing your stores and components
to refresh data based on the ContactStore.
```coffee
@unsubscribe = ContactStore.listen(@_onContactsChanged, @)
_onContactsChanged: ->
# refresh your contact results
```
Public: ContactStore provides convenience methods for searching contacts and
formatting contacts. When Contacts become editable, this store will be expanded
with additional actions.
Section: Stores
###
class ContactStore extends NylasStore
constructor: ->
if NylasEnv.isMainWindow() or NylasEnv.inSpecMode()
@_contactCache = []
@_registerListeners()
@_registerObservables()
_registerListeners: ->
@listenTo ContactRankingStore, @_sortContactsCacheWithRankings
_registerObservables: =>
# TODO I'm a bit worried about how big a cache this might be
@disposable?.dispose()
query = DatabaseStore.findAll(Contact)
@_disposable = Rx.Observable.fromQuery(query).subscribe(@_onContactsChanged)
_onContactsChanged: (contacts) =>
@_contactCache = [].concat(contacts)
@_sortContactsCacheWithRankings()
@trigger()
@_rankedContacts = []
@listenTo ContactRankingStore, => @_updateRankedContactCache()
@_updateRankedContactCache()
# Public: Search the user's contact list for the given search term.
# This method compares the `search` string against each Contact's
@ -67,55 +40,45 @@ class ContactStore extends NylasStore
# Returns an {Array} of matching {Contact} models
#
searchContacts: (search, options={}) =>
{limit, noPromise} = options
if not NylasEnv.isMainWindow()
if noPromise
throw new Error("We search Contacts in the Main window, which makes it impossible for this to be a noPromise method from this window")
# Returns a promise that resolves to the value of searchContacts
return WindowBridge.runInMainWindow("ContactStore", "searchContacts", [search, options])
if not search or search.length is 0
if noPromise
return []
else
return Promise.resolve([])
{limit} = options
limit ?= 5
limit = Math.max(limit, 0)
search = search.toLowerCase()
accountCount = AccountStore.accounts().length
matchFunction = (contact) ->
# For the time being, we never return contacts that are missing
# email addresses
return false unless contact.email
# - email (bengotow@gmail.com)
# - email domain (test@bengotow.com)
# - name parts (Ben, Go)
# - name full (Ben Gotow)
# (necessary so user can type more than first name ie: "Ben Go")
if contact.email
i = contact.email.toLowerCase().indexOf(search)
return true if i is 0 or i is contact.email.indexOf('@') + 1
if contact.name
return true if contact.name.toLowerCase().indexOf(search) is 0
return Promise.resolve([]) if not search or search.length is 0
name = contact.name?.toLowerCase() ? ""
for namePart in name.split(/\s/)
return true if namePart.indexOf(search) is 0
false
# Search ranked contacts which are stored in order in memory
results = []
for contact in @_rankedContacts
if (contact.email.toLowerCase().indexOf(search) isnt -1 or
contact.name.toLowerCase().indexOf(search) isnt -1)
results.push(contact)
if results.length is limit
return Promise.resolve(results)
matches = []
# If we haven't found enough items in memory, query for more from the
# database. Note that we ask for LIMIT * accountCount because we want to
# return contacts with distinct email addresses, and the same contact
# could exist in every account. Rather than make SQLite do a SELECT DISTINCT
# (which is very slow), we just ask for more items.
query = DatabaseStore.findAll(Contact).whereAny([
Contact.attributes.name.like(search),
Contact.attributes.email.like(search)
])
query.limit(limit * accountCount)
query.then (queryResults) =>
existingEmails = _.pluck(results, 'email')
for contact in @_contactCache
if matchFunction(contact)
matches.push(contact)
if matches.length is limit
break
# remove query results that were already found in ranked contacts
queryResults = _.reject queryResults, (c) -> c.email in existingEmails
queryResults = @_distinctByEmail(queryResults)
if noPromise
return matches
else
return Promise.resolve(matches)
results = results.concat(queryResults)
results.length = limit if results.length > limit
return Promise.resolve(results)
# Public: Returns true if the contact provided is a {Contact} instance and
# contains a properly formatted email address.
@ -132,9 +95,6 @@ class ContactStore extends NylasStore
parseContactsInString: (contactString, options={}) =>
{skipNameLookup} = options
if not NylasEnv.isMainWindow()
# Returns a promise that resolves to the value of searchContacts
return WindowBridge.runInMainWindow("ContactStore", "parseContactsInString", [contactString, options])
detected = []
emailRegex = RegExpUtils.emailRegex()
@ -159,37 +119,56 @@ class ContactStore extends NylasStore
nameStart = i+1 if i+1 > nameStart
name = contactString.substr(nameStart, match.index - 1 - nameStart).trim()
if (not name or name.length is 0) and not skipNameLookup
# Look to see if we can find a name for this email address in the ContactStore.
# Otherwise, just populate the name with the email address.
existing = @searchContacts(email, {limit:1, noPromise: true})[0]
if existing and existing.name
name = existing.name
else
name = email
# The "nameStart" for the next match must begin after lastMatchEnd
lastMatchEnd = match.index+email.length
if hasTrailingParen
lastMatchEnd += 1
if name
# If the first and last character of the name are quotation marks, remove them
[first,...,last] = name
if first in ['"', "'"] and last in ['"', "'"]
name = name[1...-1]
if not name or name.length is 0
name = email
# If the first and last character of the name are quotation marks, remove them
[firstChar,...,lastChar] = name
if firstChar in ['"', "'"] and lastChar in ['"', "'"]
name = name[1...-1]
detected.push(new Contact({email, name}))
return Promise.resolve(detected)
if skipNameLookup
return Promise.resolve(detected)
_sortContactsCacheWithRankings: =>
Promise.all detected.map (contact) =>
return contact if contact.name isnt contact.email
@searchContacts(contact.email, {limit: 1}).then ([match]) =>
return match if match and match.email is contact.email
return contact
_updateRankedContactCache: =>
rankings = ContactRankingStore.valuesForAllAccounts()
@_contactCache = _.sortBy @_contactCache, (contact) =>
(- (rankings[contact.email.toLowerCase()] ? 0) / 1)
emails = Object.keys(rankings)
if emails.length is 0
@_rankedContacts = []
return
DatabaseStore.findAll(Contact, {email: emails}).then (contacts) =>
contacts = @_distinctByEmail(contacts)
for contact in contacts
contact._rank = (- (rankings[contact.email.toLowerCase()] ? 0) / 1)
@_rankedContacts = _.sortBy contacts, (contact) -> contact._rank
_distinctByEmail: (contacts) =>
# remove query results that are duplicates, prefering ones that have names
uniq = {}
for contact in contacts
key = contact.email.toLowerCase()
existing = uniq[key]
if not existing or (not existing.name or existing.name is existing.email)
uniq[key] = contact
_.values(uniq)
_resetCache: =>
@_contactCache = {}
@_rankedContacts = []
ContactRankingStore.reset()
@trigger(@)

View file

@ -23,7 +23,6 @@ DatabasePhase =
DEBUG_TO_LOG = false
DEBUG_QUERY_PLANS = NylasEnv.inDevMode()
DEBUG_MISSING_ACCOUNT_ID = false
BEGIN_TRANSACTION = 'BEGIN TRANSACTION'
COMMIT = 'COMMIT'
@ -244,8 +243,6 @@ class DatabaseStore extends NylasStore
fn = 'run'
if query.indexOf("SELECT ") is 0
if DEBUG_MISSING_ACCOUNT_ID and query.indexOf("`account_id`") is -1
@_prettyConsoleLog("QUERY does not specify accountId: #{query}")
if DEBUG_QUERY_PLANS
@_db.all "EXPLAIN QUERY PLAN #{query}", values, (err, results=[]) =>
str = results.map((row) -> row.detail).join('\n') + " for " + query