mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-24 17:26:06 +08:00
9d995ded67
Summary: Move sync workers and Edgehill token checks to work window Move the task queue and database setup to the work window Move ContactStore background refresh to work window Store the task queue in the database WIP The TaskQueue now puts tasks in the database instead of in a file, which also means it can be observed Move all delta sync and initial sync to a package, make NylasSyncStore which exposes read-only sync state DraftStore no longer reads task status. Once you set the "sending" bit on a draft, it never gets unset. But that's fine actually. If your package lists windowTypes, you *only* get loaded in those windowTypes. If you specify no windowTypes, you get loaded in the root window. This means that onboarding, worker-ui, worker-sync, etc. no longer get loaded into the main window ActivitySidebar has a special little store that observes the task queue since it's no longer in the window Move "toggle component regions" / "toggle react remote" to the Developer menu Move sync worker specs, update draft store specs to not rely on TaskQueue at all Test Plan: Run existing tests, all pass Reviewers: dillon, evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D1936
487 lines
16 KiB
CoffeeScript
487 lines
16 KiB
CoffeeScript
path = require 'path'
|
|
|
|
_ = require 'underscore'
|
|
EmitterMixin = require('emissary').Emitter
|
|
{Emitter} = require 'event-kit'
|
|
fs = require 'fs-plus'
|
|
Q = require 'q'
|
|
Grim = require 'grim'
|
|
|
|
ServiceHub = require 'service-hub'
|
|
Package = require './package'
|
|
ThemePackage = require './theme-package'
|
|
DatabaseStore = require './flux/stores/database-store'
|
|
|
|
# Extended: Package manager for coordinating the lifecycle of Atom packages.
|
|
#
|
|
# An instance of this class is always available as the `atom.packages` global.
|
|
#
|
|
# Packages can be loaded, activated, and deactivated, and unloaded:
|
|
# * Loading a package reads and parses the package's metadata and resources
|
|
# such as keymaps, menus, stylesheets, etc.
|
|
# * Activating a package registers the loaded resources and calls `activate()`
|
|
# on the package's main module.
|
|
# * Deactivating a package unregisters the package's resources and calls
|
|
# `deactivate()` on the package's main module.
|
|
# * Unloading a package removes it completely from the package manager.
|
|
#
|
|
# Packages can be enabled/disabled via the `core.disabledPackages` config
|
|
# settings and also by calling `enablePackage()/disablePackage()`.
|
|
#
|
|
# Section: Atom
|
|
module.exports =
|
|
class PackageManager
|
|
EmitterMixin.includeInto(this)
|
|
|
|
constructor: ({configDirPath, @devMode, safeMode, @resourcePath, @specMode}) ->
|
|
@emitter = new Emitter
|
|
@packageDirPaths = []
|
|
if @specMode
|
|
@packageDirPaths.push(path.join(@resourcePath, "spec-nylas", "fixtures", "packages"))
|
|
else
|
|
@packageDirPaths.push(path.join(@resourcePath, "internal_packages"))
|
|
if not safeMode
|
|
if @devMode
|
|
@packageDirPaths.push(path.join(configDirPath, "dev", "packages"))
|
|
@packageDirPaths.push(path.join(configDirPath, "packages"))
|
|
|
|
@loadedPackages = {}
|
|
@packagesWithDatabaseObjects = []
|
|
@activePackages = {}
|
|
@packageStates = {}
|
|
@serviceHub = new ServiceHub
|
|
|
|
@packageActivators = []
|
|
@registerPackageActivator(this, ['atom', 'textmate'])
|
|
|
|
###
|
|
Section: Event Subscription
|
|
###
|
|
|
|
# Public: Invoke the given callback when all packages have been loaded.
|
|
#
|
|
# * `callback` {Function}
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidLoadInitialPackages: (callback) ->
|
|
@emitter.on 'did-load-initial-packages', callback
|
|
@emitter.on 'did-load-all', callback # TODO: Remove once deprecated pre-1.0 APIs are gone
|
|
|
|
onDidLoadAll: (callback) ->
|
|
Grim.deprecate("Use `::onDidLoadInitialPackages` instead.")
|
|
@onDidLoadInitialPackages(callback)
|
|
|
|
# Public: Invoke the given callback when all packages have been activated.
|
|
#
|
|
# * `callback` {Function}
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidActivateInitialPackages: (callback) ->
|
|
@emitter.on 'did-activate-initial-packages', callback
|
|
@emitter.on 'did-activate-all', callback # TODO: Remove once deprecated pre-1.0 APIs are gone
|
|
|
|
onDidActivateAll: (callback) ->
|
|
Grim.deprecate("Use `::onDidActivateInitialPackages` instead.")
|
|
@onDidActivateInitialPackages(callback)
|
|
|
|
# Public: Invoke the given callback when a package is activated.
|
|
#
|
|
# * `callback` A {Function} to be invoked when a package is activated.
|
|
# * `package` The {Package} that was activated.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidActivatePackage: (callback) ->
|
|
@emitter.on 'did-activate-package', callback
|
|
|
|
# Public: Invoke the given callback when a package is deactivated.
|
|
#
|
|
# * `callback` A {Function} to be invoked when a package is deactivated.
|
|
# * `package` The {Package} that was deactivated.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidDeactivatePackage: (callback) ->
|
|
@emitter.on 'did-deactivate-package', callback
|
|
|
|
# Public: Invoke the given callback when a package is loaded.
|
|
#
|
|
# * `callback` A {Function} to be invoked when a package is loaded.
|
|
# * `package` The {Package} that was loaded.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidLoadPackage: (callback) ->
|
|
@emitter.on 'did-load-package', callback
|
|
|
|
# Public: Invoke the given callback when a package is unloaded.
|
|
#
|
|
# * `callback` A {Function} to be invoked when a package is unloaded.
|
|
# * `package` The {Package} that was unloaded.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidUnloadPackage: (callback) ->
|
|
@emitter.on 'did-unload-package', callback
|
|
|
|
on: (eventName) ->
|
|
switch eventName
|
|
when 'loaded'
|
|
Grim.deprecate 'Use PackageManager::onDidLoadInitialPackages instead'
|
|
when 'activated'
|
|
Grim.deprecate 'Use PackageManager::onDidActivateInitialPackages instead'
|
|
else
|
|
Grim.deprecate 'PackageManager::on is deprecated. Use event subscription methods instead.'
|
|
EmitterMixin::on.apply(this, arguments)
|
|
|
|
###
|
|
Section: Package system data
|
|
###
|
|
|
|
# Public: Get the path to the apm command.
|
|
#
|
|
# Return a {String} file path to apm.
|
|
getApmPath: ->
|
|
return @apmPath if @apmPath?
|
|
|
|
commandName = 'apm'
|
|
commandName += '.cmd' if process.platform is 'win32'
|
|
|
|
@apmPath = path.join(process.resourcesPath, 'app', 'apm', 'bin', commandName)
|
|
if not fs.isFileSync(@apmPath)
|
|
@apmPath = path.join(@resourcePath, 'apm', 'bin', commandName)
|
|
if not fs.isFileSync(@apmPath)
|
|
@apmPath = path.join(@resourcePath, 'apm', 'node_modules', 'atom-package-manager', 'bin', commandName)
|
|
console.log(@apmPath)
|
|
@apmPath
|
|
|
|
# Public: Get the paths being used to look for packages.
|
|
#
|
|
# Returns an {Array} of {String} directory paths.
|
|
getPackageDirPaths: ->
|
|
_.clone(@packageDirPaths)
|
|
|
|
###
|
|
Section: General package data
|
|
###
|
|
|
|
# Public: Resolve the given package name to a path on disk.
|
|
#
|
|
# * `name` - The {String} package name.
|
|
#
|
|
# Return a {String} folder path or undefined if it could not be resolved.
|
|
resolvePackagePath: (name) ->
|
|
return name if fs.isDirectorySync(name)
|
|
|
|
packagePath = fs.resolve(@packageDirPaths..., name)
|
|
return packagePath if fs.isDirectorySync(packagePath)
|
|
|
|
packagePath = path.join(@resourcePath, 'node_modules', name)
|
|
return packagePath if @hasAtomEngine(packagePath)
|
|
|
|
# Public: Is the package with the given name bundled with Atom?
|
|
#
|
|
# * `name` - The {String} package name.
|
|
#
|
|
# Returns a {Boolean}.
|
|
isBundledPackage: (name) ->
|
|
@getPackageDependencies().hasOwnProperty(name)
|
|
|
|
###
|
|
Section: Enabling and disabling packages
|
|
###
|
|
|
|
# Public: Enable the package with the given name.
|
|
#
|
|
# Returns the {Package} that was enabled or null if it isn't loaded.
|
|
enablePackage: (name) ->
|
|
pack = @loadPackage(name)
|
|
pack?.enable()
|
|
pack
|
|
|
|
# Public: Disable the package with the given name.
|
|
#
|
|
# Returns the {Package} that was disabled or null if it isn't loaded.
|
|
disablePackage: (name) ->
|
|
pack = @loadPackage(name)
|
|
pack?.disable()
|
|
pack
|
|
|
|
# Public: Is the package with the given name disabled?
|
|
#
|
|
# * `name` - The {String} package name.
|
|
#
|
|
# Returns a {Boolean}.
|
|
isPackageDisabled: (name) ->
|
|
_.include(atom.config.get('core.disabledPackages') ? [], name)
|
|
|
|
###
|
|
Section: Accessing active packages
|
|
###
|
|
|
|
# Public: Get an {Array} of all the active {Package}s.
|
|
getActivePackages: ->
|
|
_.values(@activePackages)
|
|
|
|
# Public: Get the active {Package} with the given name.
|
|
#
|
|
# * `name` - The {String} package name.
|
|
#
|
|
# Returns a {Package} or undefined.
|
|
getActivePackage: (name) ->
|
|
@activePackages[name]
|
|
|
|
# Public: Is the {Package} with the given name active?
|
|
#
|
|
# * `name` - The {String} package name.
|
|
#
|
|
# Returns a {Boolean}.
|
|
isPackageActive: (name) ->
|
|
@getActivePackage(name)?
|
|
|
|
###
|
|
Section: Accessing loaded packages
|
|
###
|
|
|
|
# Public: Get an {Array} of all the loaded {Package}s
|
|
getLoadedPackages: ->
|
|
_.values(@loadedPackages)
|
|
|
|
# Get packages for a certain package type
|
|
#
|
|
# * `types` an {Array} of {String}s like ['atom', 'textmate'].
|
|
getLoadedPackagesForTypes: (types) ->
|
|
pack for pack in @getLoadedPackages() when pack.getType() in types
|
|
|
|
# Public: Get the loaded {Package} with the given name.
|
|
#
|
|
# * `name` - The {String} package name.
|
|
#
|
|
# Returns a {Package} or undefined.
|
|
getLoadedPackage: (name) ->
|
|
@loadedPackages[name]
|
|
|
|
# Public: Is the package with the given name loaded?
|
|
#
|
|
# * `name` - The {String} package name.
|
|
#
|
|
# Returns a {Boolean}.
|
|
isPackageLoaded: (name) ->
|
|
@getLoadedPackage(name)?
|
|
|
|
###
|
|
Section: Accessing available packages
|
|
###
|
|
|
|
# Public: Get an {Array} of {String}s of all the available package paths.
|
|
#
|
|
# If the optional windowType is passed, it will only load packages
|
|
# that declare that windowType in their package.json
|
|
getAvailablePackagePaths: (windowType) ->
|
|
packagePaths = []
|
|
|
|
loadPackagesWhenNoTypesSpecified = windowType is 'default'
|
|
|
|
for packageDirPath in @packageDirPaths
|
|
for packagePath in fs.listSync(packageDirPath)
|
|
# Ignore files in package directory
|
|
continue unless fs.isDirectorySync(packagePath)
|
|
# Ignore .git in package directory
|
|
continue if path.basename(packagePath)[0] is '.'
|
|
packagePaths.push(packagePath)
|
|
|
|
if windowType
|
|
packagePaths = _.filter packagePaths, (packagePath) ->
|
|
try
|
|
{windowTypes} = Package.loadMetadata(packagePath) ? {}
|
|
if windowTypes
|
|
return windowTypes[windowType]?
|
|
else if loadPackagesWhenNoTypesSpecified
|
|
return true
|
|
return false
|
|
catch
|
|
return false
|
|
|
|
packagesPath = path.join(@resourcePath, 'node_modules')
|
|
for packageName, packageVersion of @getPackageDependencies()
|
|
packagePath = path.join(packagesPath, packageName)
|
|
packagePaths.push(packagePath) if fs.isDirectorySync(packagePath)
|
|
|
|
_.uniq(packagePaths)
|
|
|
|
# Public: Get an {Array} of {String}s of all the available package names.
|
|
getAvailablePackageNames: ->
|
|
_.uniq _.map @getAvailablePackagePaths(), (packagePath) -> path.basename(packagePath)
|
|
|
|
# Public: Get an {Array} of {String}s of all the available package metadata.
|
|
getAvailablePackageMetadata: ->
|
|
packages = []
|
|
for packagePath in @getAvailablePackagePaths()
|
|
name = path.basename(packagePath)
|
|
metadata = @getLoadedPackage(name)?.metadata ? Package.loadMetadata(packagePath, true)
|
|
packages.push(metadata)
|
|
packages
|
|
|
|
###
|
|
Section: Private
|
|
###
|
|
|
|
getPackageState: (name) ->
|
|
@packageStates[name]
|
|
|
|
setPackageState: (name, state) ->
|
|
@packageStates[name] = state
|
|
|
|
getPackageDependencies: ->
|
|
unless @packageDependencies?
|
|
try
|
|
metadataPath = path.join(@resourcePath, 'package.json')
|
|
{@packageDependencies} = JSON.parse(fs.readFileSync(metadataPath)) ? {}
|
|
@packageDependencies ?= {}
|
|
|
|
@packageDependencies
|
|
|
|
hasAtomEngine: (packagePath) ->
|
|
metadata = Package.loadMetadata(packagePath, true)
|
|
metadata?.engines?.atom?
|
|
|
|
unobserveDisabledPackages: ->
|
|
@disabledPackagesSubscription?.dispose()
|
|
@disabledPackagesSubscription = null
|
|
|
|
observeDisabledPackages: ->
|
|
@disabledPackagesSubscription ?= atom.config.onDidChange 'core.disabledPackages', ({newValue, oldValue}) =>
|
|
packagesToEnable = _.difference(oldValue, newValue)
|
|
packagesToDisable = _.difference(newValue, oldValue)
|
|
|
|
@deactivatePackage(packageName) for packageName in packagesToDisable when @getActivePackage(packageName)
|
|
|
|
for packageName in packagesToEnable
|
|
@loadPackage(packageName)
|
|
|
|
@refreshDatabaseSchema()
|
|
|
|
for packageName in packagesToEnable
|
|
@activatePackage(packageName)
|
|
|
|
null
|
|
|
|
# If a windowType is passed, we'll only load packages who declare that
|
|
# windowType as `true` in their package.json file.
|
|
loadPackages: (windowType) ->
|
|
# Ensure atom exports is already in the require cache so the load time
|
|
# of the first package isn't skewed by being the first to require atom
|
|
require '../exports/atom'
|
|
|
|
packagePaths = @getAvailablePackagePaths(windowType)
|
|
|
|
packagePaths = packagePaths.filter (packagePath) => not @isPackageDisabled(path.basename(packagePath))
|
|
packagePaths = _.uniq packagePaths, (packagePath) -> path.basename(packagePath)
|
|
@loadPackage(packagePath) for packagePath in packagePaths
|
|
@emit 'loaded'
|
|
@emitter.emit 'did-load-initial-packages'
|
|
|
|
loadPackage: (nameOrPath) ->
|
|
return pack if pack = @getLoadedPackage(nameOrPath)
|
|
|
|
if packagePath = @resolvePackagePath(nameOrPath)
|
|
name = path.basename(nameOrPath)
|
|
return pack if pack = @getLoadedPackage(name)
|
|
|
|
try
|
|
metadata = Package.loadMetadata(packagePath) ? {}
|
|
if metadata.theme
|
|
pack = new ThemePackage(packagePath, metadata)
|
|
else
|
|
pack = new Package(packagePath, metadata)
|
|
pack.load()
|
|
if pack.declaresNewDatabaseObjects
|
|
@packagesWithDatabaseObjects.push pack
|
|
@loadedPackages[pack.name] = pack
|
|
@emitter.emit 'did-load-package', pack
|
|
return pack
|
|
catch error
|
|
console.warn "Failed to load package.json '#{path.basename(packagePath)}'"
|
|
console.warn error.stack ? error
|
|
else
|
|
console.warn "Could not resolve '#{nameOrPath}' to a package path"
|
|
null
|
|
|
|
unloadPackages: ->
|
|
@unloadPackage(name) for name in _.keys(@loadedPackages)
|
|
null
|
|
|
|
unloadPackage: (name) ->
|
|
if @isPackageActive(name)
|
|
throw new Error("Tried to unload active package '#{name}'")
|
|
|
|
if pack = @getLoadedPackage(name)
|
|
delete @loadedPackages[pack.name]
|
|
@emitter.emit 'did-unload-package', pack
|
|
else
|
|
throw new Error("No loaded package for name '#{name}'")
|
|
|
|
# Activate all the packages that should be activated.
|
|
activate: ->
|
|
promises = []
|
|
for [activator, types] in @packageActivators
|
|
packages = @getLoadedPackagesForTypes(types)
|
|
promises = promises.concat(activator.activatePackages(packages))
|
|
Q.all(promises).then =>
|
|
@emit 'activated'
|
|
@emitter.emit 'did-activate-initial-packages'
|
|
|
|
# another type of package manager can handle other package types.
|
|
# See ThemeManager
|
|
registerPackageActivator: (activator, types) ->
|
|
@packageActivators.push([activator, types])
|
|
|
|
activatePackages: (packages) ->
|
|
promises = []
|
|
atom.config.transact =>
|
|
for pack in packages
|
|
@loadPackage(pack.name)
|
|
|
|
@refreshDatabaseSchema()
|
|
|
|
for pack in packages
|
|
promise = @activatePackage(pack.name)
|
|
promises.push(promise) unless pack.hasActivationCommands()
|
|
@observeDisabledPackages()
|
|
promises
|
|
|
|
# When packages load they can declare new DatabaseObjects that need to
|
|
# be setup in the Database. It's important that the Database starts
|
|
# getting setup before packages activate so any DB queries in the
|
|
# `activate` methods get properly queued then executed.
|
|
#
|
|
# When a package with database-altering changes loads, it will put an
|
|
# entry in `packagesWithDatabaseObjects`.
|
|
refreshDatabaseSchema: ->
|
|
if @packagesWithDatabaseObjects.length > 0
|
|
DatabaseStore.refreshDatabaseSchema()
|
|
@packagesWithDatabaseObjects = []
|
|
|
|
# Activate a single package by name
|
|
activatePackage: (name) ->
|
|
if pack = @getActivePackage(name)
|
|
Q(pack)
|
|
else if pack = @loadPackage(name)
|
|
pack.activate().then =>
|
|
@activePackages[pack.name] = pack
|
|
@emitter.emit 'did-activate-package', pack
|
|
pack
|
|
else
|
|
Q.reject(new Error("Failed to load package '#{name}'"))
|
|
|
|
# Deactivate all packages
|
|
deactivatePackages: ->
|
|
atom.config.transact =>
|
|
@deactivatePackage(pack.name) for pack in @getLoadedPackages()
|
|
@unobserveDisabledPackages()
|
|
|
|
# Deactivate the package with the given name
|
|
deactivatePackage: (name) ->
|
|
pack = @getLoadedPackage(name)
|
|
if @isPackageActive(name)
|
|
@setPackageState(pack.name, state) if state = pack.serialize?()
|
|
pack.deactivate()
|
|
delete @activePackages[pack.name]
|
|
@emitter.emit 'did-deactivate-package', pack
|