UNPKG

@adpt/core

Version:
315 lines 13.5 kB
"use strict"; /* * Copyright 2018-2019 Unbounded Systems, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const utils_1 = require("@adpt/utils"); const fs = tslib_1.__importStar(require("fs-extra")); const ld = tslib_1.__importStar(require("lodash")); const path = tslib_1.__importStar(require("path")); const util_1 = require("util"); const dom_utils_1 = require("../dom_utils"); const error_1 = require("../error"); const jsx_1 = require("../jsx"); const packageinfo_1 = require("../packageinfo"); const ts_1 = require("../ts"); const deploy_types_1 = require("./deploy_types"); const execution_plan_1 = require("./execution_plan"); function createPluginManager(modules) { const config = createPluginConfig(modules); return new PluginManagerImpl(config); } exports.createPluginManager = createPluginManager; /** * For testing only. */ function isPluginManagerImpl(val) { return val !== null && val instanceof PluginManagerImpl; } exports.isPluginManagerImpl = isPluginManagerImpl; var PluginManagerState; (function (PluginManagerState) { PluginManagerState["Initial"] = "Initial"; PluginManagerState["Starting"] = "Starting"; PluginManagerState["PreObserve"] = "PreObserve"; PluginManagerState["Observing"] = "Observing"; PluginManagerState["PreAnalyze"] = "PreAnalyze"; PluginManagerState["Analyzing"] = "Analyzing"; PluginManagerState["PreAct"] = "PreAct"; PluginManagerState["Acting"] = "Acting"; PluginManagerState["PreFinish"] = "PreFinish"; PluginManagerState["Finishing"] = "Finishing"; })(PluginManagerState || (PluginManagerState = {})); function legalStateTransition(prev, next) { switch (prev) { case PluginManagerState.Initial: return next === PluginManagerState.Starting; case PluginManagerState.Starting: return next === PluginManagerState.PreObserve; case PluginManagerState.PreObserve: return next === PluginManagerState.Observing; case PluginManagerState.Observing: return next === PluginManagerState.PreAnalyze; case PluginManagerState.PreAnalyze: return next === PluginManagerState.Analyzing; case PluginManagerState.Analyzing: return next === PluginManagerState.PreAct; case PluginManagerState.PreAct: return [ PluginManagerState.Finishing, PluginManagerState.Acting ].find((v) => v === next) !== undefined; case PluginManagerState.Acting: return [ PluginManagerState.PreAct, PluginManagerState.PreFinish // !dryRun ].find((v) => v === next) !== undefined; case PluginManagerState.PreFinish: return next === PluginManagerState.Finishing; case PluginManagerState.Finishing: return next === PluginManagerState.Initial; } } function checkPrimitiveActions(diff, actions) { const hasPlugin = (el) => !el.componentType.noPlugin; const changes = ld.flatten(actions.map((a) => a.changes)); const done = new Set(); // The set of elements that should be claimed by plugins (i.e. referenced // in a change) is all elements in the new DOM (added+commonNew) and // all elements deleted from the old DOM, then filtered by the noPlugin // flag. const newEls = new Set([...diff.added, ...diff.commonNew].filter(hasPlugin)); const deleted = new Set([...diff.deleted].filter(hasPlugin)); changes.forEach((c) => { const el = c.element; if (!jsx_1.isMountedElement(el)) { throw new utils_1.UserError(`A plugin returned an Action with an ActionChange ` + `where the 'element' property is not a valid and mounted Element. ` + `(element=${util_1.inspect(el)})`); } if (!hasPlugin(el)) return; // Only check each el once to avoid triggering warning if el is in // more than one change. if (done.has(el)) return; done.add(el); if (!newEls.delete(el) && !deleted.delete(el)) { dom_utils_1.logElements(`WARNING: Element was specified as affected by a ` + `plugin action but was not found in old or new DOM as expected:\n` + // tslint:disable-next-line: no-console `(change: ${c.detail}): `, [el], console.log); } }); if (newEls.size > 0) { dom_utils_1.logElements(`WARNING: The following new or updated elements were ` + `not claimed by any deployment plugin and will probably not be ` + // tslint:disable-next-line: no-console `correctly deployed:\n`, [...newEls], console.log); } if (deleted.size > 0) { dom_utils_1.logElements(`WARNING: The following deleted elements were ` + `not claimed by any deployment plugin and will probably not be ` + // tslint:disable-next-line: no-console `correctly deleted:\n`, [...deleted], console.log); } } exports.checkPrimitiveActions = checkPrimitiveActions; const defaultActOptions = { dryRun: false, goalStatus: deploy_types_1.DeployOpStatus.Deployed, processStateUpdates: () => Promise.resolve({ stateChanged: false }), }; class PluginManagerImpl { constructor(config) { this.parallelActions = []; this.plugins = new Map(config.plugins); this.modules = new Map(config.modules); this.state = PluginManagerState.Initial; } transitionTo(next) { if (!legalStateTransition(this.state, next)) { throw new error_1.InternalError(`Illegal call to Plugin Manager, attempting to go from ${this.state} to ${next}`); } this.state = next; } async start(prevDom, dom, options) { this.transitionTo(PluginManagerState.Starting); this.dom = dom; this.prevDom = prevDom; this.deployment = options.deployment; this.logger = options.logger; this.observations = {}; const loptions = { deployID: options.deployment.deployID, log: options.logger.info, logger: options.logger, }; const waitingFor = utils_1.mapMap(this.plugins, async (key, plugin) => { const pMod = this.modules.get(key); if (!pMod) throw new error_1.InternalError(`no module found for ${key}`); const dataDir = pluginDataDir(options.dataDir, pMod); await fs.ensureDir(dataDir); return plugin.start(Object.assign({ dataDir }, loptions)); }); await Promise.all(waitingFor); this.transitionTo(PluginManagerState.PreObserve); } async observe() { this.transitionTo(PluginManagerState.Observing); const dom = this.dom; const prevDom = this.prevDom; if (dom === undefined || prevDom === undefined) { throw new error_1.InternalError("Must call start before observe"); } const observationsP = utils_1.mapMap(this.plugins, async (key, plugin) => ({ pluginKey: key, obs: await plugin.observe(prevDom, dom) })); const observations = await Promise.all(observationsP); const ret = {}; for (const { pluginKey: key, obs } of observations) { this.observations[key] = JSON.stringify(obs); ret[key] = obs; } this.transitionTo(PluginManagerState.PreAnalyze); return ret; } analyze() { this.transitionTo(PluginManagerState.Analyzing); const dom = this.dom; const prevDom = this.prevDom; if (dom === undefined || prevDom === undefined) { throw new error_1.InternalError("Must call start before analyze"); } this.parallelActions = []; for (const [name, plugin] of this.plugins) { const obs = JSON.parse(this.observations[name]); const actions = plugin.analyze(prevDom, dom, obs); this.addActions(actions, plugin); } if (dom && !jsx_1.isMountedElement(dom)) { throw new error_1.InternalError(`dom is not Mounted`); } if (prevDom && !jsx_1.isMountedElement(prevDom)) { throw new error_1.InternalError(`prevDom is not Mounted`); } this.diff = dom_utils_1.domDiff(prevDom, dom); checkPrimitiveActions(this.diff, this.actions); this.transitionTo(PluginManagerState.PreAct); return this.actions; } addActions(actions, plugin) { this.parallelActions = this.parallelActions.concat(actions); } /** * Creates an ExecutionPlan. Solely intended for unit testing. * @internal */ async _createExecutionPlan(options) { const { builtElements, deployOpID, goalStatus } = Object.assign({}, defaultActOptions, options); // tslint:disable-next-line: no-this-assignment const { deployment, diff } = this; if (diff == null) throw new error_1.InternalError("Must call analyze before act (diff == null)"); if (deployment == null) throw new error_1.InternalError("Must start analyze before act (deployment == null)"); const plan = await execution_plan_1.createExecutionPlan({ actions: this.parallelActions, builtElements, deployment, deployOpID, diff, goalStatus, }); plan.check(); return plan; } async act(options) { const _a = Object.assign({}, defaultActOptions, options), { builtElements, deployOpID, goalStatus } = _a, opts = tslib_1.__rest(_a, ["builtElements", "deployOpID", "goalStatus"]); // tslint:disable-next-line: no-this-assignment const { logger } = this; if (opts.taskObserver.state !== utils_1.TaskState.Started) { throw new error_1.InternalError(`PluginManager: A new TaskObserver must be provided for additional calls to act()`); } if (logger == null) throw new error_1.InternalError("Must call start before act (logger == null)"); const plan = await this._createExecutionPlan(options); this.transitionTo(PluginManagerState.Acting); const { deploymentStatus, stateChanged } = await execution_plan_1.execute(Object.assign({}, opts, { logger, plan })); if (deploymentStatus === deploy_types_1.DeployOpStatus.Failed) { throw new utils_1.UserError(`Errors encountered during plugin action phase`); } const deployComplete = deploymentStatus === goalStatus; if (!deployComplete && deploymentStatus !== deploy_types_1.DeployOpStatus.StateChanged) { throw new error_1.InternalError(`Unexpected DeployOpStatus (${deploymentStatus}) from execute`); } if (opts.dryRun) this.transitionTo(PluginManagerState.PreAct); else this.transitionTo(PluginManagerState.PreFinish); return { deployComplete, stateChanged, }; } async finish() { this.transitionTo(PluginManagerState.Finishing); const waitingFor = utils_1.mapMap(this.plugins, (_, plugin) => plugin.finish()); await Promise.all(waitingFor); this.dom = undefined; this.prevDom = undefined; this.parallelActions = []; this.logger = undefined; this.observations = {}; this.transitionTo(PluginManagerState.Initial); } get actions() { return this.parallelActions; } } function pluginKey(pMod) { return `${pMod.name} [${pMod.packageName}@${pMod.version}]`; } function pluginDataDir(dataDirRoot, pMod) { return path.join(dataDirRoot, `${pMod.packageName}@${pMod.version}`, pMod.name); } function registerPlugin(plugin) { const modules = ts_1.getAdaptContext().pluginModules; const pInfo = packageinfo_1.findPackageInfo(path.dirname(plugin.module.filename)); const mod = Object.assign({}, plugin, { packageName: pInfo.name, version: pInfo.version }); const key = pluginKey(mod); const existing = modules.get(key); if (existing !== undefined) { // Ignore if they're registering the exact same info if (existing.create === plugin.create) return; throw new Error(`Attempt to register two plugins with the same name from the ` + `same package: ${key}`); } modules.set(key, mod); } exports.registerPlugin = registerPlugin; function createPluginConfig(modules) { if (modules.size === 0) throw new Error(`No plugins registered`); const plugins = new Map(); for (const [key, mod] of modules) { plugins.set(key, mod.create()); } return { modules, plugins }; } exports.createPluginConfig = createPluginConfig; //# sourceMappingURL=plugin_support.js.map