"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Frame = void 0;
const events_1 = require("events");
const exception_js_1 = require("../exception.js");
const cdp_util_js_1 = require("./cdp-util.js");
const execution_context_js_1 = require("./execution-context.js");
const index_js_1 = require("./inject/index.js");
class Frame extends events_1.EventEmitter {
    constructor(page, frameId, parentFrame) {
        super();
        this.page = page;
        this.frameId = frameId;
        this.parentFrame = parentFrame;
        this.childFrames = new Set();
        this.loaded = false;
        this.ready = false;
        this.failed = false;
        this.url = '';
        this.securityOrigin = '';
        this.mimeType = '';
        this._isolatedWorld = null;
        this._defaultExecCtx = null;
        if (parentFrame) {
            parentFrame.childFrames.add(this);
        }
        this.clearExecutionContexts();
    }
    get logger() {
        return this.page.logger;
    }
    isMainFrame() {
        return !this.parentFrame;
    }
    async getCurrentExecutionContext() {
        if (this._isolatedWorld && this._isolatedWorld.isAlive) {
            return this._isolatedWorld;
        }
        return await this.initIsolatedWorld();
    }
    async initIsolatedWorld() {
        try {
            await this.page.waitForReady({
                rejectNetworkErrors: false,
                rejectHttpErrors: false,
                rejectTimeout: false,
                timeout: 30000,
            });
        }
        catch {
        }
        const { executionContextId } = await this.page.target.send('Page.createIsolatedWorld', {
            frameId: this.frameId,
            worldName: 'Autopilot',
            grantUniveralAccess: true,
        });
        this._isolatedWorld = new execution_context_js_1.ExecutionContext(this, executionContextId);
        await this._isolatedWorld.initContentScripts(index_js_1.runtimeScripts);
        await this.verifyToolkitLoaded(this._isolatedWorld);
        return this._isolatedWorld;
    }
    async getDefaultExecutionContext() {
        if (!this._defaultExecCtx) {
            return await this.getCurrentExecutionContext();
        }
        return this._defaultExecCtx;
    }
    clearExecutionContexts() {
        this._defaultExecCtx = null;
        this._isolatedWorld = null;
    }
    async verifyToolkitLoaded(context, maxRetries = 3) {
        const toolkitBinding = this.page.toolkitBinding;
        for (let i = 0; i < maxRetries; i++) {
            try {
                const toolkitAvailable = await context.evaluateJson((binding) => {
                    const toolkit = window[binding];
                    return toolkit &&
                        typeof toolkit.getElementInfo === 'function' &&
                        typeof toolkit.queryAll === 'function' &&
                        typeof toolkit.queryOne === 'function';
                }, toolkitBinding);
                if (toolkitAvailable) {
                    return;
                }
                await new Promise(resolve => setTimeout(resolve, 100));
            }
            catch (err) {
                if (/Cannot find context with specified id/.test(err.message)) {
                    throw err;
                }
                await new Promise(resolve => setTimeout(resolve, 100));
            }
        }
    }
    async evaluate(pageFn, ...args) {
        return await this.withRetryOnContextLoss(async (execContext) => {
            return await execContext.evaluate(pageFn, ...args);
        });
    }
    async evaluateElement(pageFn, ...args) {
        return await this.withRetryOnContextLoss(async (execContext) => {
            return await execContext.evaluateElement(pageFn, ...args);
        });
    }
    async evaluateJson(pageFn, ...args) {
        return await this.withRetryOnContextLoss(async (execContext) => {
            return await execContext.evaluateJson(pageFn, ...args);
        });
    }
    async document() {
        try {
            return (await this.evaluateElement(() => document));
        }
        catch (err) {
            throw new exception_js_1.Exception({
                name: 'PageLoadingFailed',
                message: 'Failed to obtain top frame document',
                retry: true,
                details: {
                    cause: {
                        message: err.message,
                        code: err.code,
                        details: err.details,
                    },
                },
            });
        }
    }
    async querySelectorAll(selector) {
        return (await this.document()).querySelectorAll(selector);
    }
    async querySelector(selector) {
        return (await this.document()).querySelector(selector);
    }
    onNavigated(cdpFrame) {
        this.loaded = false;
        this.ready = false;
        this.failed = !!cdpFrame.unreachableUrl;
        this.url = cdpFrame.unreachableUrl || cdpFrame.url;
        this.securityOrigin = cdpFrame.securityOrigin;
        this.mimeType = cdpFrame.mimeType;
        this.emit('navigate');
        if (['iframe', 'page'].includes(this.page.target.type)) {
            this.logger.debug(`Navigate (${this.page.target.type}) ${this.url}`, {
                frameId: this.frameId,
                url: this.url,
            });
        }
    }
    onStoppedLoading() {
        this.ready = true;
        this.loaded = true;
        this.emit('ready');
        this.emit('loaded');
    }
    async captureHtmlSnapshot() {
        await this.page.waitForReady({ timeout: 30000, rejectHttpErrors: false }).catch(error => {
            this.logger.warn('Capture html snapshot: Failed waiting for page to be ready', { error });
        });
        await this.htmlSnapshotPopulateFrameIds();
        const snapshot = await this.htmlSnapshotEvalWithFrameRefs();
        return await this.htmlSnapshotResolveFrameRefs(snapshot);
    }
    async htmlSnapshotPopulateFrameIds() {
        const frames = await this.querySelectorAll('iframe, frame');
        const nodeInfosPromises = frames.map(frame => this.page.send('DOM.describeNode', { objectId: frame.objectId }));
        const nodeInfos = await Promise.all(nodeInfosPromises);
        const frameIds = nodeInfos.map(nodeInfo => nodeInfo.node.frameId);
        await this.evaluateJson((frameIds, ...frames) => {
            for (let i = 0; i < frames.length; i++) {
                frames[i].__ubioFrameId = frameIds[i];
            }
        }, frameIds, ...frames);
    }
    async htmlSnapshotEvalWithFrameRefs() {
        try {
            return await this.evaluateJson(toolkitBinding => {
                return window[toolkitBinding].htmlSnapshot();
            }, this.page.toolkitBinding);
        }
        catch (err) {
            return await this.evaluateJson(() => document.documentElement.outerHTML);
        }
    }
    async htmlSnapshotResolveFrameRefs(snapshot) {
        const frameMarkerRegex = /------@@(.*?):\d+@@------/g;
        const result = await (0, cdp_util_js_1.asyncRegexpReplace)(snapshot, frameMarkerRegex, async (m) => {
            const frameId = m[1];
            const frame = await this.page.resolveFrameById(frameId);
            if (!frame) {
                return 'data:text;frame content unavailable';
            }
            const html = await frame.captureHtmlSnapshot();
            const htmlBase64 = Buffer.from(html).toString('base64');
            return `data:text/html;base64,${htmlBase64}"`;
        });
        return result;
    }
    async withRetryOnContextLoss(fn) {
        const maxAttempts = 3;
        let attempts = 0;
        let lastError;
        let useDefaultContext = false;
        while (attempts < maxAttempts) {
            let ctx;
            try {
                if (!useDefaultContext) {
                    ctx = await this.getCurrentExecutionContext();
                }
                else {
                    ctx = await this.getDefaultExecutionContext();
                }
            }
            catch (err) {
                if (!useDefaultContext) {
                    this.logger.warn('Isolated world failed, falling back to default context', { error: err.message });
                    useDefaultContext = true;
                    ctx = await this.getDefaultExecutionContext();
                }
                else {
                    throw err;
                }
            }
            try {
                return await fn(ctx);
            }
            catch (err) {
                lastError = err;
                const isContextError = /Cannot find context with specified id/.test(err.message) ||
                    /Execution context was destroyed/.test(err.message) ||
                    /Context.*destroyed/.test(err.message);
                const isToolkitError = /Cannot read properties of undefined/.test(err.message) &&
                    /getElementInfo|queryAll|queryOne/.test(err.message);
                if (isContextError || isToolkitError) {
                    this._isolatedWorld = null;
                    attempts += 1;
                    if (!useDefaultContext && attempts < maxAttempts) {
                        useDefaultContext = true;
                        this.logger.warn('Switching to default context due to isolated world failure', { attempt: attempts });
                    }
                    if (attempts < maxAttempts) {
                        await new Promise(resolve => setTimeout(resolve, 100));
                        continue;
                    }
                }
                throw err;
            }
        }
        throw lastError;
    }
}
exports.Frame = Frame;
//# sourceMappingURL=frame.js.map