@adpt/core
Version:
AdaptJS core library
315 lines • 13.5 kB
JavaScript
;
/*
* 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