'use strict'; // Serialises an async dispatch so that high-frequency callers cannot stack // up overlapping invocations. Intermediate values are dropped — only the // most recent fire() during an in-flight dispatch is replayed afterwards. // Extracted from machineGroupControl's _dispatchInFlight + _delayedCall // pattern so MGC, pumpingStation, valveGroupControl etc. can share it. class LatestWinsGate { constructor(asyncDispatchFn, options = {}) { if (typeof asyncDispatchFn !== 'function') { throw new TypeError('LatestWinsGate requires an async dispatch function'); } this._dispatch = asyncDispatchFn; this._logger = options.logger || null; this._inFlight = false; this._pending = null; // { value, ctx } | null this._drainResolvers = []; // resolved when idle again this.lastError = null; } // 0 = idle, 1 = running with no pending, 2 = running with pending. get size() { if (!this._inFlight) return 0; return this._pending ? 2 : 1; } // Never blocks the caller. If a dispatch is in flight, the latest // value is parked; older parked values are silently overwritten. fire(value, ctx) { if (this._inFlight) { this._pending = { value, ctx }; return; } this._run(value, ctx); } drain() { if (!this._inFlight && !this._pending) return Promise.resolve(); return new Promise((resolve) => { this._drainResolvers.push(resolve); }); } _run(value, ctx) { this._inFlight = true; // Kick the dispatch on a microtask so fire() always returns // synchronously, even if _dispatch resolves immediately. Promise.resolve() .then(() => this._dispatch(value, ctx)) .catch((err) => { this.lastError = err; if (this._logger && typeof this._logger.error === 'function') { this._logger.error(err); } // Swallow: an error must not deadlock the gate. }) .then(() => this._afterDispatch()); } _afterDispatch() { this._inFlight = false; if (this._pending) { const { value, ctx } = this._pending; this._pending = null; this._run(value, ctx); return; } // Idle — release any drain() waiters. const waiters = this._drainResolvers; this._drainResolvers = []; for (const r of waiters) r(); } } module.exports = LatestWinsGate;