// @ts-check
/**
* @file src/plugins/wait.js
* @copyright @spmhome @_2025
* @author Scott Meesseman @spmeesseman
*//** */
const WpwPlugin = require("./base");
const { GLOBAL_EVENT_BUILD_ERROR } = require("../utils/constants");
const {
capitalize, pluralize, promiseRaceEvent, isEmpty, promiseFromEvent, isDefined, apply, SpmhEventEmitter
} = require("@spmhome/type-utils");
/**
* @class WpwWaitPlugin
* @since 1.3.0
* @augments WpwPlugin
*/
class WpwWaitPlugin extends WpwPlugin
{
/**
* @static
* @type {SpmhEventEmitter}
*/
static onPluginEvent = new SpmhEventEmitter();
/**
* @static
* @private
* @type {Partial<Record<WpwBuildOptionsKey, any>>}
*/
static wtdBuild = {};
/**
* @static
* @private
* @type {Partial<Record<WpwBuildOptionsKey, WpwBuildOptionsKey>>}
*/
static waitDoneBuilds = {};
/**
* @param {WpwPluginOptions} options Plugin options to be applied
*/
constructor(options)
{
super(options);
this.buildOptions = /** @type {WpwBuildOptionsPluginConfig<"wait">} */(this.buildOptions);
}
/**
* @override
*/
static create = WpwWaitPlugin.wrap.bind(this);
/**
* @override
* @returns {WpwPluginTapOptions<any, any, boolean> | undefined | void}
*/
onApply()
{
if (!this.build.isOnlyBuild)
{
/** @type {WpwPluginTapOptions<any, any, boolean>} */
const cfg = {},
b = this.build,
w = this.build.wrapper,
o = this.buildOptions,
items = o.items?.filter((i) => !!w.getBuild(i.name, true)) || [],
isAwaited = w.builds.filter((wb) =>
wb.options.wait && wb.options.wait.enabled && !!wb.options.wait.items?.find((i) => i.name === b.name)
);
if (isAwaited.length > 0 && !isDefined(WpwWaitPlugin.wtdBuild[b.name]))
{
const textBuilds = pluralize("build", isAwaited),
waitingBuildNames = isAwaited.map((b) => b.name),
hkTag = isAwaited.map((wb) => wb.name).reduce((p, v) => p + capitalize(v), ""),
hkDoneDesc = `${pluralize("emitDoneEventToBuild", isAwaited)}${hkTag}`;
WpwWaitPlugin.wtdBuild[b.name] = {
builds: waitingBuildNames,
textWaitingBuilds: `${textBuilds} '${waitingBuildNames.join("' & '")}'`
};
apply(cfg,
{
[`emitFailEventToWaiting${textBuilds}`]: {
forceRun: true,
hook: "failed",
messageStart: hkDoneDesc,
callback: () => this.emitEventToWaitingBuilds("failed", "failed")
},
[`emitDoneEventToWaiting${textBuilds}`]: {
forceRun: true,
hook: "done",
messageStart: hkDoneDesc,
callback: () => this.emitEventToWaitingBuilds("done", "done")
},
[`emitShutdownEventToWaiting${textBuilds}`]: {
forceRun: true,
hook: "shutdown",
messageStart: hkDoneDesc,
callback: () => this.emitEventToWaitingBuilds("shutdown", "done")
}
});
}
if (items.length > 0)
{
const textBuilds = pluralize("build", isAwaited),
waitingBuildNames = items.map((i) => i.name);
WpwWaitPlugin.wtdBuild[b.name] = {
builds: waitingBuildNames,
textWaitingBuilds: `${textBuilds} '${waitingBuildNames.join("' & '")}'`
};
apply(cfg,
{
[`waitFor${capitalize(textBuilds)}ToComplete`]: {
async: true,
hook: "beforeRun",
callback: this.waitForBuildDone.bind(this)
}
});
}
return cfg;
}
}
/**
* @param {"done" | "failed" | "shutdown"} hook
* @param {"done" | "failed"} state
*/
emitEventToWaitingBuilds(hook, state)
{
const b = this.build,
l = this.build.logger,
doneEvent = `${b.name}_exit`,
status = WpwWaitPlugin.waitDoneBuilds[b.name];
this.hookstart(`emit ${doneEvent} event`);
if (!status)
{
const textWaitingBuilds = WpwWaitPlugin.wtdBuild[b.name].textWaitingBuilds;
l.write(` ${state}, notify waiting ${textWaitingBuilds}`, 1);
WpwWaitPlugin.onPluginEvent.fire(state);
WpwWaitPlugin.waitDoneBuilds[b.name] = hook;
}
else {
l.write(` skip, notification already emitted in '${status}' stage`, 1);
}
this.hookdone(`emit ${doneEvent} event`);
}
// /**
// * @private
// * @param {IWpwPluginConfigWaitItem} waitConfig
// * @returns {{ promise: Promise<void>, cancel: (() => void) }}
// */
// pollFile(waitConfig)
// {
// /** @type {NodeJS.Timeout | number} */
// let _iId,
// /** @type {NodeJS.Timeout | number} */
// _tId = 0,
// _disposed = false;
// const emitter = new EventEmitter(),
// waitFile = resolve(this.build.getBasePath(), waitConfig.name);
// if (!waitConfig.interval || waitConfig.interval < 750) {
// waitConfig.interval = 750;
// }
// const _dispose = () =>
// {
// emitter.removeAllListeners("cancel");
// if (_iId) { clearTimeout(_iId); _iId = 0; }
// if (_tId) { clearTimeout(_tId); _tId = 0; }
// };
// const _poll = (ok, fail, msWaited) =>
// {
// if (existsSync(waitFile))
// {
// _dispose();
// process.nextTick(unlinkSync, waitFile);
// ok({ data: "done", cancelled: false, timeout: false});
// }
// else if (msWaited >= (waitConfig.timeout || 45000))
// {
// _dispose();
// fail({ data: "timeout", cancelled: false, timeout: true });
// }
// else
// { if (msWaited === 0)
// { emitter.once("cancel", (reason) => {
// _dispose();
// fail({ data: reason, cancelled: true, timeout: false});
// });
// _tId = setTimeout(_poll, waitConfig.interval, ok, fail, msWaited += waitConfig.interval);
// }
// _iId = setTimeout(_poll, waitConfig.interval, ok, fail, msWaited += waitConfig.interval);
// }
// };
// return {
// promise: new Promise((ok, fail) => _poll(ok, fail, 0)),
// cancel: (reason = "event") =>
// { if (!_disposed)
// {
// _disposed = true;
// emitter.emit("cancel", reason);
// emitter.removeAllListeners("cancel");
// }
// }
// };
// }
/**
* @returns {Promise<void>}
*/
waitForBuildDone()
{
if (!this.build.isOnlyBuild)
{
const o = this.buildOptions,
w = this.build.wrapper,
waitItems = o.items?.filter((i) => !!w.getBuild(i.name, true));
if (!isEmpty(waitItems)) // && !WpwWaitPlugin.donePlugins.includes(waitItemCfg.name))
{
const b = this.build,
l = this.build.logger,
o = this.buildOptions,
timeout = o.timeout,
waitItemsFmt = WpwWaitPlugin.wtdBuild[b.name].textWaitingBuilds;
/**
* @param {WpwPluginConfigWaitItem} waitItem
* @returns {Promise<"done"|"failed">}
*/
const _wait = (waitItem) => new Promise((ok, fail) =>
{
if (waitItem.mode === "event")
{
const timeout = waitItem.timeout || this.buildOptions.timeout;
promiseFromEvent(WpwWaitPlugin.onPluginEvent, timeout).promise
// promiseFromEvent(WpwWaitPlugin.onPluginEvent, `${waitItem.name}_exit`, timeout).promise
.then((result) =>
{
const eventParts = result.result.split("_"),
waitingOns = WpwWaitPlugin.wtdBuild[b.name].builds,
waitingOnIdx = waitingOns.findIndex((bn) => bn === eventParts[0]);
l.write(` received '${result.result}' event from ${waitingOns.splice(waitingOnIdx, 1)}`, 1);
if (!b.hasGlobalError)
{
if (waitingOns.length === 0) {
l.write(" un-pause and continue, all awaited builds complete", 1);
}
else {
const textBuilds = pluralize("build", waitingOns);
l.write(` still waiting on ${textBuilds} '${waitingOns.join(" & ")}'`, 1);
}
ok(result.result);
}
else {
const failedBuild = w.builds.find((b) => b.hasError);
fail(new Error(`${GLOBAL_EVENT_BUILD_ERROR}:${failedBuild || "unknown"}`));
}
})
.catch(fail);
}
else
{ // const { promise, cancel } = this.pollFile(waitItem);
// promise.then((result) => ok(_waitDoneEvent(waitItem, result.data))).catch(fail).finally(cancel);
fail(new Error("mode 'file' not implemented"));
}
});
// const __wait = Promise.all(waitItems.map(_wait))
// .then((result) =>
// {
// l.write(` received 'exit' notification from '${waitItem.name}', un-pause and continue`, 2);
// return result.data;
// })
// .catch((e) => { throw e; });
l.write(`pause build, waiting for ${waitItemsFmt} to complete`, 1);
return promiseRaceEvent(
// Promise.all(waitItems.map(_wait)), this.global.globalEvent.emitter, GLOBAL_EVENT_BUILD_ERROR, timeout
Promise.all(waitItems.map(_wait)), this.global.globalEvent, timeout
)
.catch((e) => this.execUtils.handlePromiseEventResult(this.logger, e, `[waiting_on::${waitItemsFmt}]`, ""));
}
}
return /** @type {Promise<void>} */(new Promise((ok) => setTimeout(() => ok(), 1)));
}
}
module.exports = WpwWaitPlugin.create;