From cc9b58878bf5f88096ff6eed7d81bd678f6117bc Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 25 Nov 2019 20:28:34 -0800 Subject: [PATCH] chore: reuse WaitTask between browsers (#67) --- src/chromium/DOMWorld.ts | 182 +------------------------------ src/firefox/DOMWorld.ts | 204 +++++------------------------------ src/waitTask.ts | 182 +++++++++++++++++++++++++++++++ src/webkit/FrameManager.ts | 213 +++++-------------------------------- 4 files changed, 243 insertions(+), 538 deletions(-) create mode 100644 src/waitTask.ts diff --git a/src/chromium/DOMWorld.ts b/src/chromium/DOMWorld.ts index 544745b195..a01276903e 100644 --- a/src/chromium/DOMWorld.ts +++ b/src/chromium/DOMWorld.ts @@ -17,15 +17,15 @@ import * as fs from 'fs'; import * as types from '../types'; -import { TimeoutError } from '../Errors'; import { ExecutionContext } from './ExecutionContext'; import { Frame } from './Frame'; import { FrameManager } from './FrameManager'; -import { assert, helper } from '../helper'; +import { helper } from '../helper'; import { ElementHandle, JSHandle } from './JSHandle'; import { LifecycleWatcher } from './LifecycleWatcher'; import { TimeoutSettings } from '../TimeoutSettings'; -import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from '../input'; +import { WaitTask, WaitTaskParams } from '../waitTask'; + const readFileAsync = helper.promisify(fs.readFile); export class DOMWorld { @@ -36,7 +36,7 @@ export class DOMWorld { private _contextPromise: Promise; private _contextResolveCallback: ((c: ExecutionContext) => void) | null; private _context: ExecutionContext | null; - _waitTasks = new Set(); + _waitTasks = new Set>(); private _detached = false; constructor(frameManager: FrameManager, frame: Frame, timeoutSettings: TimeoutSettings) { @@ -366,177 +366,3 @@ export class DOMWorld { } } -type WaitTaskParams = { - predicateBody: Function | string; - title: string; - polling: string | number; - timeout: number; - args: any[]; -}; - -class WaitTask { - readonly promise: Promise; - private _cleanup: () => void; - private _params: WaitTaskParams & { predicateBody: string }; - private _runCount: number; - private _resolve: (result: JSHandle) => void; - private _reject: (reason: Error) => void; - private _timeoutTimer: NodeJS.Timer; - private _terminated: boolean; - - constructor(params: WaitTaskParams, cleanup: () => void) { - if (helper.isString(params.polling)) - assert(params.polling === 'raf' || params.polling === 'mutation', 'Unknown polling option: ' + params.polling); - else if (helper.isNumber(params.polling)) - assert(params.polling > 0, 'Cannot poll with non-positive interval: ' + params.polling); - else - throw new Error('Unknown polling options: ' + params.polling); - - this._params = { - ...params, - predicateBody: helper.isString(params.predicateBody) ? 'return (' + params.predicateBody + ')' : 'return (' + params.predicateBody + ')(...args)' - }; - this._cleanup = cleanup; - this._runCount = 0; - this.promise = new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; - }); - // Since page navigation requires us to re-install the pageScript, we should track - // timeout on our end. - if (params.timeout) { - const timeoutError = new TimeoutError(`waiting for ${params.title} failed: timeout ${params.timeout}ms exceeded`); - this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), params.timeout); - } - } - - terminate(error: Error) { - this._terminated = true; - this._reject(error); - this._doCleanup(); - } - - async rerun(context: ExecutionContext) { - const runCount = ++this._runCount; - let success: JSHandle | null = null; - let error = null; - try { - success = await context.evaluateHandle(waitForPredicatePageFunction, this._params.predicateBody, this._params.polling, this._params.timeout, ...this._params.args); - } catch (e) { - error = e; - } - - if (this._terminated || runCount !== this._runCount) { - if (success) - await success.dispose(); - return; - } - - // Ignore timeouts in pageScript - we track timeouts ourselves. - // If execution context has been already destroyed, `context.evaluate` will - // throw an error - ignore this predicate run altogether. - if (!error && await context.evaluate(s => !s, success).catch(e => true)) { - await success.dispose(); - return; - } - - // When the page is navigated, the promise is rejected. - // We will try again in the new execution context. - if (error && error.message.includes('Execution context was destroyed')) - return; - - // We could have tried to evaluate in a context which was already - // destroyed. - if (error && error.message.includes('Cannot find context with specified id')) - return; - - if (error) - this._reject(error); - else - this._resolve(success); - - this._doCleanup(); - } - - _doCleanup() { - clearTimeout(this._timeoutTimer); - this._cleanup(); - } -} - -async function waitForPredicatePageFunction(predicateBody: string, polling: string | number, timeout: number, ...args): Promise { - const predicate = new Function('...args', predicateBody); - let timedOut = false; - if (timeout) - setTimeout(() => timedOut = true, timeout); - if (polling === 'raf') - return await pollRaf(); - if (polling === 'mutation') - return await pollMutation(); - if (typeof polling === 'number') - return await pollInterval(polling); - - function pollMutation(): Promise { - const success = predicate.apply(null, args); - if (success) - return Promise.resolve(success); - - let fulfill; - const result = new Promise(x => fulfill = x); - const observer = new MutationObserver(mutations => { - if (timedOut) { - observer.disconnect(); - fulfill(); - } - const success = predicate.apply(null, args); - if (success) { - observer.disconnect(); - fulfill(success); - } - }); - observer.observe(document, { - childList: true, - subtree: true, - attributes: true - }); - return result; - } - - function pollRaf(): Promise { - let fulfill; - const result = new Promise(x => fulfill = x); - onRaf(); - return result; - - function onRaf() { - if (timedOut) { - fulfill(); - return; - } - const success = predicate.apply(null, args); - if (success) - fulfill(success); - else - requestAnimationFrame(onRaf); - } - } - - function pollInterval(pollInterval: number): Promise { - let fulfill; - const result = new Promise(x => fulfill = x); - onTimeout(); - return result; - - function onTimeout() { - if (timedOut) { - fulfill(); - return; - } - const success = predicate.apply(null, args); - if (success) - fulfill(success); - else - setTimeout(onTimeout, pollInterval); - } - } -} diff --git a/src/firefox/DOMWorld.ts b/src/firefox/DOMWorld.ts index d2c523222d..0418ee7f6f 100644 --- a/src/firefox/DOMWorld.ts +++ b/src/firefox/DOMWorld.ts @@ -15,13 +15,12 @@ * limitations under the License. */ -import {helper, assert} from '../helper'; -import {TimeoutError} from '../Errors'; import * as fs from 'fs'; import * as util from 'util'; import * as types from '../types'; import {ElementHandle, JSHandle} from './JSHandle'; import { ExecutionContext } from './ExecutionContext'; +import { WaitTaskParams, WaitTask } from '../waitTask'; const readFileAsync = util.promisify(fs.readFile); @@ -31,7 +30,8 @@ export class DOMWorld { _documentPromise: any; _contextPromise: any; _contextResolveCallback: any; - _waitTasks: Set; + private _context: ExecutionContext | null; + _waitTasks: Set>; _detached: boolean; constructor(frame, timeoutSettings) { this._frame = frame; @@ -50,12 +50,13 @@ export class DOMWorld { return this._frame; } - _setContext(context) { + _setContext(context: ExecutionContext) { + this._context = context; if (context) { this._contextResolveCallback.call(null, context); this._contextResolveCallback = null; for (const waitTask of this._waitTasks) - waitTask.rerun(); + waitTask.rerun(context); } else { this._documentPromise = null; this._contextPromise = new Promise(fulfill => { @@ -250,7 +251,14 @@ export class DOMWorld { polling = 'raf', timeout = this._timeoutSettings.timeout(), } = options; - return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise; + const params: WaitTaskParams = { + predicateBody: pageFunction, + title: 'function', + polling, + timeout, + args + }; + return this._scheduleWaitTask(params); } async title(): Promise { @@ -265,8 +273,14 @@ export class DOMWorld { } = options; const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation'; const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${waitForHidden ? ' to be hidden' : ''}`; - const waitTask = new WaitTask(this, predicate, title, polling, timeout, selectorOrXPath, isXPath, waitForVisible, waitForHidden); - const handle = await waitTask.promise; + const params: WaitTaskParams = { + predicateBody: predicate, + title, + polling, + timeout, + args: [selectorOrXPath, isXPath, waitForVisible, waitForHidden] + }; + const handle = await this._scheduleWaitTask(params); if (!handle.asElement()) { await handle.dispose(); return null; @@ -294,174 +308,12 @@ export class DOMWorld { } } } -} -class WaitTask { - promise: any; - _domWorld: any; - _polling: any; - _timeout: any; - _predicateBody: string; - _args: any[]; - _runCount: number; - _resolve: (value?: unknown) => void; - _reject: (reason?: any) => void; - _timeoutTimer: NodeJS.Timer; - _terminated: boolean; - _runningTask: any; - constructor(domWorld: DOMWorld, predicateBody: Function | string, title, polling: string | number, timeout: number, ...args: Array) { - if (helper.isString(polling)) - assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling); - else if (helper.isNumber(polling)) - assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling); - else - throw new Error('Unknown polling options: ' + polling); - - this._domWorld = domWorld; - this._polling = polling; - this._timeout = timeout; - this._predicateBody = helper.isString(predicateBody) ? 'return (' + predicateBody + ')' : 'return (' + predicateBody + ')(...args)'; - this._args = args; - this._runCount = 0; - domWorld._waitTasks.add(this); - this.promise = new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; - }); - // Since page navigation requires us to re-install the pageScript, we should track - // timeout on our end. - if (timeout) { - const timeoutError = new TimeoutError(`waiting for ${title} failed: timeout ${timeout}ms exceeded`); - this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout); - } - this.rerun(); - } - - terminate(error: Error) { - this._terminated = true; - this._reject(error); - this._cleanup(); - } - - async rerun() { - const runCount = ++this._runCount; - let success: JSHandle | null = null; - let error = null; - try { - success = await this._domWorld.evaluateHandle(waitForPredicatePageFunction, this._predicateBody, this._polling, this._timeout, ...this._args); - } catch (e) { - error = e; - } - - if (this._terminated || runCount !== this._runCount) { - if (success) - await success.dispose(); - return; - } - - // Ignore timeouts in pageScript - we track timeouts ourselves. - // If the frame's execution context has already changed, `frame.evaluate` will - // throw an error - ignore this predicate run altogether. - if (!error && await this._domWorld.evaluate(s => !s, success).catch(e => true)) { - await success.dispose(); - return; - } - - // When the page is navigated, the promise is rejected. - // Try again right away. - if (error && error.message.includes('Execution context was destroyed')) { - this.rerun(); - return; - } - - if (error) - this._reject(error); - else - this._resolve(success); - - this._cleanup(); - } - - _cleanup() { - clearTimeout(this._timeoutTimer); - this._domWorld._waitTasks.delete(this); - this._runningTask = null; - } -} - -async function waitForPredicatePageFunction(predicateBody: string, polling: string, timeout: number, ...args): Promise { - const predicate = new Function('...args', predicateBody); - let timedOut = false; - if (timeout) - setTimeout(() => timedOut = true, timeout); - if (polling === 'raf') - return await pollRaf(); - if (polling === 'mutation') - return await pollMutation(); - if (typeof polling === 'number') - return await pollInterval(polling); - - function pollMutation(): Promise { - const success = predicate.apply(null, args); - if (success) - return Promise.resolve(success); - - let fulfill; - const result = new Promise(x => fulfill = x); - const observer = new MutationObserver(mutations => { - if (timedOut) { - observer.disconnect(); - fulfill(); - } - const success = predicate.apply(null, args); - if (success) { - observer.disconnect(); - fulfill(success); - } - }); - observer.observe(document, { - childList: true, - subtree: true, - attributes: true - }); - return result; - } - - function pollRaf(): Promise { - let fulfill; - const result = new Promise(x => fulfill = x); - onRaf(); - return result; - - function onRaf() { - if (timedOut) { - fulfill(); - return; - } - const success = predicate.apply(null, args); - if (success) - fulfill(success); - else - requestAnimationFrame(onRaf); - } - } - - function pollInterval(pollInterval: number): Promise { - let fulfill; - const result = new Promise(x => fulfill = x); - onTimeout(); - return result; - - function onTimeout() { - if (timedOut) { - fulfill(); - return; - } - const success = predicate.apply(null, args); - if (success) - fulfill(success); - else - setTimeout(onTimeout, pollInterval); - } + private _scheduleWaitTask(params: WaitTaskParams): Promise { + const task = new WaitTask(params, () => this._waitTasks.delete(task)); + this._waitTasks.add(task); + if (this._context) + task.rerun(this._context); + return task.promise; } } diff --git a/src/waitTask.ts b/src/waitTask.ts new file mode 100644 index 0000000000..6064cd4c56 --- /dev/null +++ b/src/waitTask.ts @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert, helper } from './helper'; +import * as types from './types'; +import { TimeoutError } from './Errors'; + +export type WaitTaskParams = { + // TODO: ensure types. + predicateBody: Function | string; + title: string; + polling: string | number; + timeout: number; + args: any[]; +}; + +export class WaitTask { + readonly promise: Promise; + private _cleanup: () => void; + private _params: WaitTaskParams & { predicateBody: string }; + private _runCount: number; + private _resolve: (result: Handle) => void; + private _reject: (reason: Error) => void; + private _timeoutTimer: NodeJS.Timer; + private _terminated: boolean; + + constructor(params: WaitTaskParams, cleanup: () => void) { + if (helper.isString(params.polling)) + assert(params.polling === 'raf' || params.polling === 'mutation', 'Unknown polling option: ' + params.polling); + else if (helper.isNumber(params.polling)) + assert(params.polling > 0, 'Cannot poll with non-positive interval: ' + params.polling); + else + throw new Error('Unknown polling options: ' + params.polling); + + this._params = { + ...params, + predicateBody: helper.isString(params.predicateBody) ? 'return (' + params.predicateBody + ')' : 'return (' + params.predicateBody + ')(...args)' + }; + this._cleanup = cleanup; + this._runCount = 0; + this.promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + // Since page navigation requires us to re-install the pageScript, we should track + // timeout on our end. + if (params.timeout) { + const timeoutError = new TimeoutError(`waiting for ${params.title} failed: timeout ${params.timeout}ms exceeded`); + this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), params.timeout); + } + } + + terminate(error: Error) { + this._terminated = true; + this._reject(error); + this._doCleanup(); + } + + async rerun(context: types.EvaluationContext) { + const runCount = ++this._runCount; + let success: Handle | null = null; + let error = null; + try { + success = await context.evaluateHandle(waitForPredicatePageFunction, this._params.predicateBody, this._params.polling, this._params.timeout, ...this._params.args); + } catch (e) { + error = e; + } + + if (this._terminated || runCount !== this._runCount) { + if (success) + await success.dispose(); + return; + } + + // Ignore timeouts in pageScript - we track timeouts ourselves. + // If execution context has been already destroyed, `context.evaluate` will + // throw an error - ignore this predicate run altogether. + if (!error && await context.evaluate(s => !s, success).catch(e => true)) { + await success.dispose(); + return; + } + + // When the page is navigated, the promise is rejected. + // We will try again in the new execution context. + if (error && error.message.includes('Execution context was destroyed')) + return; + + // We could have tried to evaluate in a context which was already + // destroyed. + if (error && error.message.includes('Cannot find context with specified id')) + return; + + if (error) + this._reject(error); + else + this._resolve(success); + + this._doCleanup(); + } + + _doCleanup() { + clearTimeout(this._timeoutTimer); + this._cleanup(); + } +} + +async function waitForPredicatePageFunction(predicateBody: string, polling: string | number, timeout: number, ...args): Promise { + const predicate = new Function('...args', predicateBody); + let timedOut = false; + if (timeout) + setTimeout(() => timedOut = true, timeout); + if (polling === 'raf') + return await pollRaf(); + if (polling === 'mutation') + return await pollMutation(); + if (typeof polling === 'number') + return await pollInterval(polling); + + function pollMutation(): Promise { + const success = predicate.apply(null, args); + if (success) + return Promise.resolve(success); + + let fulfill; + const result = new Promise(x => fulfill = x); + const observer = new MutationObserver(mutations => { + if (timedOut) { + observer.disconnect(); + fulfill(); + } + const success = predicate.apply(null, args); + if (success) { + observer.disconnect(); + fulfill(success); + } + }); + observer.observe(document, { + childList: true, + subtree: true, + attributes: true + }); + return result; + } + + function pollRaf(): Promise { + let fulfill; + const result = new Promise(x => fulfill = x); + onRaf(); + return result; + + function onRaf() { + if (timedOut) { + fulfill(); + return; + } + const success = predicate.apply(null, args); + if (success) + fulfill(success); + else + requestAnimationFrame(onRaf); + } + } + + function pollInterval(pollInterval: number): Promise { + let fulfill; + const result = new Promise(x => fulfill = x); + onTimeout(); + return result; + + function onTimeout() { + if (timedOut) { + fulfill(); + return; + } + const success = predicate.apply(null, args); + if (success) + fulfill(success); + else + setTimeout(onTimeout, pollInterval); + } + } +} diff --git a/src/webkit/FrameManager.ts b/src/webkit/FrameManager.ts index 2026c911d0..d78f513fb2 100644 --- a/src/webkit/FrameManager.ts +++ b/src/webkit/FrameManager.ts @@ -27,6 +27,8 @@ import { NetworkManager, NetworkManagerEvents, Request, Response } from './Netwo import { Page } from './Page'; import { Protocol } from './protocol'; import { MultiClickOptions, ClickOptions, SelectOption } from '../input'; +import { WaitTask, WaitTaskParams } from '../waitTask'; + const readFileAsync = helper.promisify(fs.readFile); export const FrameManagerEvents = { @@ -238,8 +240,8 @@ export class Frame { _detached: boolean; _loaderId: string; _lifecycleEvents: Set; - _waitTasks: Set; - _executionContext: ExecutionContext; + _waitTasks: Set>; + _executionContext: ExecutionContext | null; _contextPromise: Promise; _contextResolveCallback: (arg: ExecutionContext) => void; _childFrames: Set; @@ -300,7 +302,14 @@ export class Frame { polling = 'raf', timeout = this._frameManager._timeoutSettings.timeout(), } = options; - return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise; + const params: WaitTaskParams = { + predicateBody: pageFunction, + title: 'function', + polling, + timeout, + args + }; + return this._scheduleWaitTask(params); } async executionContext(): Promise { @@ -606,8 +615,14 @@ export class Frame { } = options; const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation'; const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${waitForHidden ? ' to be hidden' : ''}`; - const waitTask = new WaitTask(this, predicate, title, polling, timeout, selectorOrXPath, isXPath, waitForVisible, waitForHidden); - const handle = await waitTask.promise; + const params: WaitTaskParams = { + predicateBody: predicate, + title, + polling, + timeout, + args: [selectorOrXPath, isXPath, waitForVisible, waitForHidden] + }; + const handle = await this._scheduleWaitTask(params); if (!handle.asElement()) { await handle.dispose(); return null; @@ -676,7 +691,7 @@ export class Frame { this._contextResolveCallback.call(null, context); this._contextResolveCallback = null; for (const waitTask of this._waitTasks) - waitTask.rerun(); + waitTask.rerun(context); } else { this._documentPromise = null; this._contextPromise = new Promise(fulfill => { @@ -684,6 +699,14 @@ export class Frame { }); } } + + private _scheduleWaitTask(params: WaitTaskParams): Promise { + const task = new WaitTask(params, () => this._waitTasks.delete(task)); + this._waitTasks.add(task); + if (this._executionContext) + task.rerun(this._executionContext); + return task.promise; + } } /** @@ -773,181 +796,3 @@ class NextNavigationWatchdog { clearTimeout(this._timeoutId); } } - -class WaitTask { - promise: Promise; - _domWorld: any; - _polling: string | number; - _timeout: number; - _predicateBody: string; - _args: any[]; - _runCount: number; - _resolve: (value?: unknown) => void; - _reject: (reason?: any) => void; - _timeoutTimer: NodeJS.Timer; - _terminated: boolean; - _runningTask: any; - constructor(domWorld: Frame, predicateBody: Function | string, title, polling: string | number, timeout: number, ...args: Array) { - if (helper.isString(polling)) - assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling); - else if (helper.isNumber(polling)) - assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling); - else - throw new Error('Unknown polling options: ' + polling); - - this._domWorld = domWorld; - this._polling = polling; - this._timeout = timeout; - this._predicateBody = helper.isString(predicateBody) ? 'return (' + predicateBody + ')' : 'return (' + predicateBody + ')(...args)'; - this._args = args; - this._runCount = 0; - domWorld._waitTasks.add(this); - this.promise = new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; - }); - // Since page navigation requires us to re-install the pageScript, we should track - // timeout on our end. - if (timeout) { - const timeoutError = new TimeoutError(`waiting for ${title} failed: timeout ${timeout}ms exceeded`); - this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout); - } - this.rerun(); - } - - terminate(error: Error) { - this._terminated = true; - this._reject(error); - this._cleanup(); - } - - async rerun() { - const runCount = ++this._runCount; - /** @type {?JSHandle} */ - let success: JSHandle | null = null; - let error = null; - try { - success = await (await this._domWorld.executionContext()).evaluateHandle(waitForPredicatePageFunction, this._predicateBody, this._polling, this._timeout, ...this._args); - } catch (e) { - error = e; - } - - if (this._terminated || runCount !== this._runCount) { - if (success) - await success.dispose(); - return; - } - - // Ignore timeouts in pageScript - we track timeouts ourselves. - // If the frame's execution context has already changed, `frame.evaluate` will - // throw an error - ignore this predicate run altogether. - if (!error && await this._domWorld.evaluate(s => !s, success).catch(e => true)) { - await success.dispose(); - return; - } - - // When the page is navigated, the promise is rejected. - // We will try again in the new execution context. - if (error && error.message.includes('Execution context was destroyed')) - return; - // In WebKit cross-process navigation leads to targetDestroyed (followed - // by targetCreated). - if (error && error.message.includes('Target closed')) - return; - - // We could have tried to evaluate in a context which was already - // destroyed. - if (error && error.message.includes('Cannot find context with specified id')) - return; - - if (error) - this._reject(error); - else - this._resolve(success); - - this._cleanup(); - } - - _cleanup() { - clearTimeout(this._timeoutTimer); - this._domWorld._waitTasks.delete(this); - this._runningTask = null; - } -} - -async function waitForPredicatePageFunction(predicateBody: string, polling: string, timeout: number, ...args): Promise { - const predicate = new Function('...args', predicateBody); - let timedOut = false; - if (timeout) - setTimeout(() => timedOut = true, timeout); - if (polling === 'raf') - return await pollRaf(); - if (polling === 'mutation') - return await pollMutation(); - if (typeof polling === 'number') - return await pollInterval(polling); - - function pollMutation(): Promise { - const success = predicate.apply(null, args); - if (success) - return Promise.resolve(success); - - let fulfill; - const result = new Promise(x => fulfill = x); - const observer = new MutationObserver(mutations => { - if (timedOut) { - observer.disconnect(); - fulfill(); - } - const success = predicate.apply(null, args); - if (success) { - observer.disconnect(); - fulfill(success); - } - }); - observer.observe(document, { - childList: true, - subtree: true, - attributes: true - }); - return result; - } - - function pollRaf(): Promise { - let fulfill; - const result = new Promise(x => fulfill = x); - onRaf(); - return result; - - function onRaf() { - if (timedOut) { - fulfill(); - return; - } - const success = predicate.apply(null, args); - if (success) - fulfill(success); - else - requestAnimationFrame(onRaf); - } - } - - function pollInterval(pollInterval: number): Promise { - let fulfill; - const result = new Promise(x => fulfill = x); - onTimeout(); - return result; - - function onTimeout() { - if (timedOut) { - fulfill(); - return; - } - const success = predicate.apply(null, args); - if (success) - fulfill(success); - else - setTimeout(onTimeout, pollInterval); - } - } -}