plugins_wait.js

// @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;