openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
310 lines • 12.4 kB
JavaScript
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
if (value !== null && value !== void 0) {
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
var dispose, inner;
if (async) {
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
dispose = value[Symbol.asyncDispose];
}
if (dispose === void 0) {
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
dispose = value[Symbol.dispose];
if (async) inner = dispose;
}
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
env.stack.push({ value: value, dispose: dispose, async: async });
}
else if (async) {
env.stack.push({ async: true });
}
return value;
};
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
return function (env) {
function fail(e) {
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
env.hasError = true;
}
var r, s = 0;
function next() {
while (r = env.stack.pop()) {
try {
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
if (r.dispose) {
var result = r.dispose.call(r.value);
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
}
else s |= 1;
}
catch (e) {
fail(e);
}
}
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
if (env.hasError) throw env.error;
}
return next();
};
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
});
import { logger } from './logger.js';
export class WaitForHelper {
#abortController = new AbortController();
#page;
#stableDomTimeout;
#stableDomFor;
#expectNavigationIn;
#navigationTimeout;
#dialogHandled = false;
/** Track all dialogs as they pause the renderer. */
#dialogDetected = false;
#initialUrl;
constructor(page, cpuTimeoutMultiplier, networkTimeoutMultiplier) {
this.#stableDomTimeout = 3000 * cpuTimeoutMultiplier;
this.#stableDomFor = 100 * cpuTimeoutMultiplier;
this.#expectNavigationIn = 100 * cpuTimeoutMultiplier;
this.#navigationTimeout = 3000 * networkTimeoutMultiplier;
this.#page = page;
this.#initialUrl = page.url();
}
/**
* A wrapper that executes a action and waits for
* a potential navigation, after which it waits
* for the DOM to be stable before returning.
*/
async waitForStableDom() {
const env_1 = { stack: [], error: void 0, hasError: false };
try {
// Bound the setup evaluation against the stable-DOM timeout. Without this
// cap a paused renderer (e.g. an open dialog) would make evaluateHandle
// hang until protocolTimeout (default 180s) while the tool mutex is held.
const stableDomObserver = __addDisposableResource(env_1, await Promise.race([
this.#page.evaluateHandle(timeout => {
let timeoutId;
function callback() {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
domObserver.resolver.resolve();
domObserver.observer.disconnect();
}, timeout);
}
const domObserver = {
resolver: Promise.withResolvers(),
observer: new MutationObserver(callback),
};
// It's possible that the DOM is not gonna change so we
// need to start the timeout initially.
callback();
domObserver.observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
});
return domObserver;
}, this.#stableDomFor),
this.timeout(this.#stableDomTimeout),
]).catch(() => undefined), false);
if (!stableDomObserver) {
return;
}
this.#abortController.signal.addEventListener('abort', async () => {
try {
await stableDomObserver.evaluate(observer => {
observer.observer.disconnect();
observer.resolver.resolve();
});
}
catch {
// Ignored cleanup errors
}
});
return Promise.race([
stableDomObserver.evaluate(async (observer) => {
return await observer.resolver.promise;
}),
this.timeout(this.#stableDomTimeout).then(() => {
throw new Error('Timeout');
}),
]);
}
catch (e_1) {
env_1.error = e_1;
env_1.hasError = true;
}
finally {
__disposeResources(env_1);
}
}
timeout(time) {
return new Promise(res => {
const id = setTimeout(res, time);
this.#abortController.signal.addEventListener('abort', () => {
res();
clearTimeout(id);
});
});
}
async waitForEventsAfterAction(action, options) {
if (this.#abortController.signal.aborted) {
throw new Error("Can't re-use a WaitForHelper");
}
const dialogHandler = (dialog) => {
this.#dialogDetected = true;
if (!options?.handleDialog) {
return;
}
let actionToTake;
if (typeof options.handleDialog === 'object') {
actionToTake = options.handleDialog[dialog.type()];
}
else {
actionToTake = options.handleDialog;
}
if (actionToTake) {
this.#dialogHandled = true;
if (actionToTake === 'dismiss') {
void dialog.dismiss();
}
else if (actionToTake === 'accept') {
void dialog.accept();
}
else {
void dialog.accept(actionToTake);
}
}
};
this.#page.on('dialog', dialogHandler);
this.#abortController.signal.addEventListener('abort', () => {
this.#page.off('dialog', dialogHandler);
});
// A scoped AbortController used to clean up navigation probe listeners.
// When aborted (either after navigation detection finishes or if this.#abortController
// aborts), it removes CDP navigation probe listeners and automatically
// detaches the abort listener from this.#abortController.signal.
const navigationAbortController = new AbortController();
const navigationStartedResolvers = Promise.withResolvers();
const navigationListener = (event) => {
if (event.frameId !== this.#page.mainFrame()._id) {
return;
}
if (event.navigationType === 'sameDocument' ||
event.navigationType === 'historySameDocument') {
return;
}
navigationStartedResolvers.resolve(true);
};
const requestedNavigationListener = (event) => {
if (event.frameId === this.#page.mainFrame()._id) {
navigationStartedResolvers.resolve(true);
}
};
this.#page._client().on('Page.frameStartedNavigating', navigationListener);
this.#page
._client()
.on('Page.frameRequestedNavigation', requestedNavigationListener);
navigationAbortController.signal.addEventListener('abort', () => {
this.#page
._client()
.off('Page.frameStartedNavigating', navigationListener);
this.#page
._client()
.off('Page.frameRequestedNavigation', requestedNavigationListener);
});
this.#abortController.signal.addEventListener('abort', () => {
navigationStartedResolvers.resolve(false);
navigationAbortController.abort();
}, { signal: navigationAbortController.signal });
// Puppeteer's waitForNavigation must be started before the action runs so that
// it captures the pre-action loader ID. If started after the action triggers navigation,
// it risks recording the new loader ID and hanging until timeout.
// If no navigation occurs, this.#abortController will cancel it in the finally block.
const navigationFinished = this.#page
.waitForNavigation({
timeout: options?.timeout ?? this.#navigationTimeout,
signal: this.#abortController.signal,
ignoreSameDocumentNavigation: true,
})
.then(result => {
navigationStartedResolvers.resolve(true);
return result;
})
.catch(error => {
if (this.#abortController.signal.aborted ||
(error instanceof Error && error.name === 'AbortError')) {
return;
}
logger?.(error);
});
try {
await action();
}
catch (error) {
// Clear up pending promises
this.#abortController.abort();
throw error;
}
const expectNavigationIn = options?.expectNavigationIn ?? this.#expectNavigationIn;
const navigationStarted = await Promise.race([
navigationStartedResolvers.promise,
this.timeout(expectNavigationIn).then(() => {
navigationStartedResolvers.resolve(false);
return false;
}),
]);
navigationAbortController.abort();
try {
// Only await navigation if one was actually initiated; otherwise, the
// pending waitForNavigation promise will be cancelled when this.#abortController aborts.
if (navigationStarted) {
await navigationFinished;
}
if (this.#dialogDetected) {
return this.#getResult();
}
// Wait for stable dom after navigation so we execute in
// the correct context
if (options?.waitForStableDom !== false) {
await this.waitForStableDom();
}
}
catch (error) {
logger?.(error);
}
finally {
this.#abortController.abort();
}
return this.#getResult();
}
#getResult() {
const urlAfterAction = this.#page.url();
return {
...(urlAfterAction !== this.#initialUrl
? { navigatedToUrl: urlAfterAction }
: {}),
dialogHandled: this.#dialogHandled,
};
}
}
export function getNetworkMultiplierFromString(condition) {
const puppeteerCondition = condition;
switch (puppeteerCondition) {
case 'Fast 4G':
return 1;
case 'Slow 4G':
return 2.5;
case 'Fast 3G':
return 5;
case 'Slow 3G':
return 10;
}
return 1;
}
//# sourceMappingURL=WaitForHelper.js.map