UNPKG

@adpt/core

Version:
371 lines 16.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 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