@adpt/core
Version:
AdaptJS core library
371 lines • 16.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 debug_1 = tslib_1.__importDefault(require("debug"));
const path = tslib_1.__importStar(require("path"));
// @ts-ignore
// tslint:disable-next-line:variable-name prefer-const
let Adapt;
const utils_1 = require("@adpt/utils");
const plugin_support_1 = require("../deploy/plugin_support");
const dom_1 = require("../dom");
const dom_build_data_recorder_1 = require("../dom_build_data_recorder");
const error_1 = require("../error");
const history_1 = require("../server/history");
const state_1 = require("../state");
const ts_1 = require("../ts");
const common_1 = require("./common");
const serialize_1 = require("./serialize");
const debugAction = debug_1.default("adapt:ops:action");
const debugDeployDom = debug_1.default("adapt:ops:deploydom");
function computePaths(options) {
const fileName = path.resolve(options.fileName);
const projectRoot = options.projectRoot || path.dirname(fileName);
return { fileName, projectRoot };
}
exports.computePaths = computePaths;
async function currentState(options) {
const { deployment } = options;
let lastCommit;
const paths = computePaths(options);
const prev = await deployment.lastEntry(history_1.HistoryStatus.complete);
const observationsJson = options.observationsJson ||
(prev ? prev.observationsJson : "{}");
const prevStateJson = options.prevStateJson ||
(prev ? prev.stateJson : "{}");
let stateStore;
try {
stateStore = state_1.createStateStore(prevStateJson);
}
catch (err) {
let msg = `Invalid previous state JSON`;
if (err.message)
msg += `: ${err.message}`;
throw new Error(msg);
}
// Allocate a new opID for this operation if not provided
const deployOpID = options.deployOpID !== undefined ?
options.deployOpID : await deployment.newOpID();
const stackName = options.stackName || (prev && prev.stackName);
if (!stackName) {
throw new Error(`stackName option not provided and previous ` +
`stackName not present`);
}
const ret = Object.assign({}, options, paths, { commit,
observationsJson, prevDomXml: prev && prev.domXml, prevStateJson,
deployOpID,
stackName,
stateStore, withStatus: options.withStatus || false });
return ret;
async function commit(entry) {
if (lastCommit === history_1.HistoryStatus.preAct &&
entry.status === history_1.HistoryStatus.preAct)
return;
if (lastCommit && history_1.isStatusComplete(lastCommit)) {
throw new error_1.InternalError(`Attempt to commit a repeated final ` +
`HistoryStatus (${entry.status})`);
}
lastCommit = entry.status;
if (!ret.dryRun) {
await deployment.commitEntry(Object.assign({}, entry, { fileName: ret.fileName, projectRoot: ret.projectRoot, stackName: ret.stackName, stateJson: stateStore.serialize() }));
}
}
}
exports.currentState = currentState;
function podify(x) {
return JSON.parse(JSON.stringify(x));
}
async function build(options) {
return withContext(options, async (ctx) => {
const { deployment, taskObserver, stackName, stateStore } = options;
const logger = taskObserver.logger;
const observations = serialize_1.parseFullObservationsJson(options.observationsJson);
const observerObservations = observations.observer || {};
const debugFlags = common_1.parseDebugString(options.debug);
const recorder = debugFlags.build ? dom_build_data_recorder_1.buildPrinter() : undefined;
// This is the inner context's copy of Adapt
const inAdapt = ctx.Adapt;
const stacks = ctx.adaptStacks;
if (!stacks)
throw new error_1.InternalError(`No stacks found`);
const stack = stacks.get(stackName);
if (!stack)
throw new utils_1.UserError(`Adapt stack '${stackName}' not found`);
let builtElements = [];
let mountedOrig = null;
let newDom = null;
let executedQueries = {};
let processStateUpdates = () => Promise.resolve({ stateChanged: false });
let needsData = {};
const root = await stack.root;
const style = await stack.style;
if (root != null) {
const observeManager = inAdapt.internal.makeObserverManagerDeployment(observerObservations);
const results = await inAdapt.build(root, style, {
deployID: deployment.deployID,
deployOpID: options.deployOpID,
observerManager: observeManager,
recorder,
stateStore,
});
if (results.buildErr || dom_1.isBuildOutputPartial(results)) {
logger.append(results.messages);
throw new error_1.ProjectBuildError(inAdapt.serializeDom(results.contents));
}
builtElements = results.builtElements;
newDom = results.contents;
mountedOrig = results.mountedOrig;
executedQueries = podify(observeManager.executedQueries());
needsData = podify(observeManager.executedQueriesThatNeededData());
processStateUpdates = results.processStateUpdates;
}
return Object.assign({}, options, { builtElements,
ctx, domXml: inAdapt.serializeDom(newDom, { reanimateable: true }), mountedOrigStatus: (mountedOrig && options.withStatus) ?
podify(await mountedOrig.status()) : { noStatus: true }, needsData,
newDom,
executedQueries, prevStateJson: stateStore.serialize(), processStateUpdates });
});
}
exports.build = build;
async function observe(options) {
debugAction(`observe: start`);
const ret = withContext(options, async (ctx) => {
const { taskObserver } = options;
const logger = taskObserver.logger;
const origObservations = serialize_1.parseFullObservationsJson(options.observationsJson);
// This is the inner context's copy of Adapt
const inAdapt = ctx.Adapt;
debugAction(`observe: run observers`);
const observations = await inAdapt.internal.observe(options.executedQueries, logger);
inAdapt.internal.patchInNewQueries(observations, options.executedQueries);
const { executedQueries } = options, orig = tslib_1.__rest(options, ["executedQueries"]);
return Object.assign({}, orig, { observationsJson: serialize_1.stringifyFullObservations({
plugin: origObservations.plugin,
observer: observations
}) });
});
debugAction(`observe: done`);
return ret;
}
exports.observe = observe;
async function withContext(options, f) {
let ctx = options.ctx;
if (ctx === undefined) {
// Compile and run the project
debugAction(`buildAndDeploy: compile start`);
const task = options.taskObserver.childGroup().task("compile");
ctx = await task.complete(() => ts_1.projectExec(options.projectRoot, options.fileName));
debugAction(`buildAndDeploy: compile done`);
}
return f(ctx);
}
exports.withContext = withContext;
async function reanimateAndBuild(opts) {
const inAdapt = opts.ctx.Adapt;
const { deployID, deployOpID, domXml, logger } = opts;
let stateStore;
try {
stateStore = state_1.createStateStore(opts.stateJson);
}
catch (err) {
let msg = `Invalid state JSON during reanimate`;
if (err.message)
msg += `: ${err.message}`;
throw new Error(msg);
}
const zombie = await inAdapt.internal.reanimateDom(domXml, deployID, deployOpID);
if (zombie === null)
return null;
const buildRes = await inAdapt.build(zombie, null, {
deployID,
deployOpID,
buildOnce: true,
stateStore,
});
if (buildRes.messages.length > 0)
logger.append(buildRes.messages);
if (dom_1.isBuildOutputError(buildRes)) {
throw new Error(`Error attempting to rebuild reanimated DOM`);
}
if (dom_1.isBuildOutputPartial(buildRes)) {
throw new Error(`Rebuilding reanimated DOM produced a partial build`);
}
const checkXML = inAdapt.serializeDom(buildRes.contents, { reanimateable: true });
if (checkXML !== domXml) {
logger.error(`Error comparing reanimated built dom to original:\n` +
`Original:\n` + domXml + `\nCheck:\n` + checkXML);
throw new Error(`Error comparing reanimated built dom to original`);
}
return buildRes.contents;
}
async function deployPass(options) {
const { actTaskObserver, dataDir, deployment, prevDom, taskObserver } = options, buildOpts = tslib_1.__rest(options, ["actTaskObserver", "dataDir", "deployment", "prevDom", "taskObserver"]);
return withContext(options, async (ctx) => {
// This is the inner context's copy of Adapt
const inAdapt = ctx.Adapt;
debugAction(`deployPass: rebuild`);
taskObserver.updateStatus("Rebuilding DOM");
const buildResults = await build(Object.assign({}, buildOpts, { deployment, withStatus: true, taskObserver }));
const { newDom, processStateUpdates } = buildResults;
if (debugDeployDom.enabled) {
debugDeployDom(inAdapt.serializeDom(newDom, { props: ["key"] }));
}
debugAction(`deployPass: observe`);
taskObserver.updateStatus("Observing and analyzing environment");
const mgr = plugin_support_1.createPluginManager(ctx.pluginModules);
await mgr.start(prevDom, newDom, {
dataDir: path.join(dataDir, "plugins"),
deployment,
logger: actTaskObserver.logger,
});
const newPluginObs = await mgr.observe();
debugAction(`deployPass: analyze`);
mgr.analyze();
const observationsJson = serialize_1.stringifyFullObservations({
plugin: newPluginObs,
observer: serialize_1.parseFullObservationsJson(options.observationsJson).observer
});
/*
* NOTE: There should be no deployment side effects prior to here, but
* once act is called the first time, that is no longer true.
*/
let status = history_1.HistoryStatus.preAct;
await commit();
status = history_1.HistoryStatus.failed;
try {
debugAction(`deployPass: act`);
taskObserver.updateStatus("Applying changes to environment");
if (actTaskObserver.state === utils_1.TaskState.Created) {
actTaskObserver.started();
}
const { deployComplete, stateChanged } = await mgr.act({
builtElements: buildResults.builtElements,
deployOpID: options.deployOpID,
dryRun: options.dryRun,
processStateUpdates,
taskObserver: actTaskObserver,
});
await mgr.finish();
debugAction(`deployPass: done (complete: ${deployComplete}, state changed: ${stateChanged})`);
return Object.assign({}, buildResults, { deployComplete,
stateChanged,
observationsJson });
}
catch (err) {
await commit();
throw err;
}
async function commit() {
await options.commit({
status,
dataDir,
domXml: buildResults.domXml,
observationsJson,
});
}
});
}
exports.deployPass = deployPass;
async function buildAndDeploy(options) {
debugAction(`buildAndDeploy: start`);
const initial = await currentState(options);
const topTask = options.taskObserver;
const tasks = topTask.childGroup().add({
compile: "Compiling project",
build: "Building new DOM",
reanimatePrev: "Loading previous DOM",
observe: "Observing environment",
deploy: "Deploying",
});
return withContext(initial, async (ctx) => {
const { commit, deployment, stateStore } = initial;
const deployID = deployment.deployID;
// This is the inner context's copy of Adapt
const inAdapt = ctx.Adapt;
const deployTasks = tasks.deploy.childGroup({ serial: false }).add({
status: "Deployment progress",
act: "Applying changes to environment",
});
try {
debugAction(`buildAndDeploy: build deployOpID: ${initial.deployOpID}`);
const build1 = await tasks.build.complete(() => build(Object.assign({}, initial, { ctx, withStatus: false, taskObserver: tasks.build })));
debugAction(`buildAndDeploy: reanimate`);
const prevDom = await tasks.reanimatePrev.complete(async () => {
return initial.prevDomXml ?
reanimateAndBuild({
ctx,
deployOpID: initial.deployOpID,
domXml: initial.prevDomXml,
stateJson: initial.prevStateJson,
deployID,
logger: tasks.reanimatePrev.logger,
}) : null;
});
debugAction(`buildAndDeploy: observe`);
const observeOptions = Object.assign({}, build1, { taskObserver: tasks.observe });
const obs = await tasks.observe.complete(() => observe(observeOptions));
debugAction(`buildAndDeploy: deploy`);
const result = await tasks.deploy.complete(() => deployTasks.status.complete(async () => {
try {
const dataDir = await deployment.getDataDir(history_1.HistoryStatus.complete);
const { needsData } = obs, fromBuild = tslib_1.__rest(obs, ["needsData"]);
const passOpts = Object.assign({}, fromBuild, { actTaskObserver: deployTasks.act, dataDir,
prevDom, taskObserver: deployTasks.status });
while (true) {
const res = await deployPass(passOpts);
if (!res.deployComplete && !res.stateChanged) {
throw new Error(`TODO: Need to implement retry/timeout still`);
}
if (res.deployComplete && !res.stateChanged) {
await commit({
status: history_1.HistoryStatus.success,
dataDir,
domXml: res.domXml,
observationsJson: res.observationsJson,
});
deployTasks.act.complete();
return res;
}
}
}
catch (err) {
deployTasks.act.failed(err);
throw err;
}
}));
debugAction(`buildAndDeploy: done`);
const logger = topTask.logger;
return {
type: "success",
deployID: initial.dryRun ? "DRYRUN" : deployment.deployID,
domXml: result.domXml,
stateJson: stateStore.serialize(),
//Move data from inner adapt to outer adapt via JSON
needsData: podify(inAdapt.internal.simplifyNeedsData(result.needsData)),
messages: logger.messages,
summary: logger.summary,
mountedOrigStatus: result.mountedOrigStatus,
};
}
finally {
await deployment.releaseDataDir();
}
});
}
exports.buildAndDeploy = buildAndDeploy;
//# sourceMappingURL=buildAndDeploy.js.map