12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404 |
- // Licensed to the Software Freedom Conservancy (SFC) under one
- // or more contributor license agreements. See the NOTICE file
- // distributed with this work for additional information
- // regarding copyright ownership. The SFC licenses this file
- // to you under the Apache License, Version 2.0 (the
- // "License"); you may not use this file except in compliance
- // with the License. You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing,
- // software distributed under the License is distributed on an
- // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- // KIND, either express or implied. See the License for the
- // specific language governing permissions and limitations
- // under the License.
- /**
- * @fileoverview
- *
- * > ### IMPORTANT NOTICE
- * >
- * > The promise manager contained in this module is in the process of being
- * > phased out in favor of native JavaScript promises. This will be a long
- * > process and will not be completed until there have been two major LTS Node
- * > releases (approx. Node v10.0) that support
- * > [async functions](https://tc39.github.io/ecmascript-asyncawait/).
- * >
- * > At this time, the promise manager can be disabled by setting an environment
- * > variable, `SELENIUM_PROMISE_MANAGER=0`. In the absence of async functions,
- * > users may use generators with the
- * > {@link ./promise.consume promise.consume()} function to write "synchronous"
- * > style tests:
- * >
- * > ```js
- * > const {Builder, By, Key, promise, until} = require('selenium-webdriver');
- * >
- * > let result = promise.consume(function* doGoogleSearch() {
- * > let driver = new Builder().forBrowser('firefox').build();
- * > yield driver.get('http://www.google.com/ncr');
- * > yield driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN);
- * > yield driver.wait(until.titleIs('webdriver - Google Search'), 1000);
- * > yield driver.quit();
- * > });
- * >
- * > result.then(_ => console.log('SUCCESS!'),
- * > e => console.error('FAILURE: ' + e));
- * > ```
- * >
- * > The motivation behind this change and full deprecation plan are documented
- * > in [issue 2969](https://github.com/SeleniumHQ/selenium/issues/2969).
- * >
- * >
- *
- * The promise module is centered around the {@linkplain ControlFlow}, a class
- * that coordinates the execution of asynchronous tasks. The ControlFlow allows
- * users to focus on the imperative commands for their script without worrying
- * about chaining together every single asynchronous action, which can be
- * tedious and verbose. APIs may be layered on top of the control flow to read
- * as if they were synchronous. For instance, the core
- * {@linkplain ./webdriver.WebDriver WebDriver} API is built on top of the
- * control flow, allowing users to write
- *
- * driver.get('http://www.google.com/ncr');
- * driver.findElement({name: 'q'}).sendKeys('webdriver', Key.RETURN);
- *
- * instead of
- *
- * driver.get('http://www.google.com/ncr')
- * .then(function() {
- * return driver.findElement({name: 'q'});
- * })
- * .then(function(q) {
- * return q.sendKeys('webdriver', Key.RETURN);
- * });
- *
- * ## Tasks and Task Queues
- *
- * The control flow is based on the concept of tasks and task queues. Tasks are
- * functions that define the basic unit of work for the control flow to execute.
- * Each task is scheduled via {@link ControlFlow#execute()}, which will return
- * a {@link ManagedPromise} that will be resolved with the task's result.
- *
- * A task queue contains all of the tasks scheduled within a single turn of the
- * [JavaScript event loop][JSEL]. The control flow will create a new task queue
- * the first time a task is scheduled within an event loop.
- *
- * var flow = promise.controlFlow();
- * flow.execute(foo); // Creates a new task queue and inserts foo.
- * flow.execute(bar); // Inserts bar into the same queue as foo.
- * setTimeout(function() {
- * flow.execute(baz); // Creates a new task queue and inserts baz.
- * }, 0);
- *
- * Whenever the control flow creates a new task queue, it will automatically
- * begin executing tasks in the next available turn of the event loop. This
- * execution is [scheduled as a microtask][MicrotasksArticle] like e.g. a
- * (native) `Promise.then()` callback.
- *
- * setTimeout(() => console.log('a'));
- * Promise.resolve().then(() => console.log('b')); // A native promise.
- * flow.execute(() => console.log('c'));
- * Promise.resolve().then(() => console.log('d'));
- * setTimeout(() => console.log('fin'));
- * // b
- * // c
- * // d
- * // a
- * // fin
- *
- * In the example above, b/c/d is logged before a/fin because native promises
- * and this module use "microtask" timers, which have a higher priority than
- * "macrotasks" like `setTimeout`.
- *
- * ## Task Execution
- *
- * Upon creating a task queue, and whenever an existing queue completes a task,
- * the control flow will schedule a microtask timer to process any scheduled
- * tasks. This ensures no task is ever started within the same turn of the
- * JavaScript event loop in which it was scheduled, nor is a task ever started
- * within the same turn that another finishes.
- *
- * When the execution timer fires, a single task will be dequeued and executed.
- * There are several important events that may occur while executing a task
- * function:
- *
- * 1. A new task queue is created by a call to {@link ControlFlow#execute()}.
- * Any tasks scheduled within this task queue are considered subtasks of the
- * current task.
- * 2. The task function throws an error. Any scheduled tasks are immediately
- * discarded and the task's promised result (previously returned by
- * {@link ControlFlow#execute()}) is immediately rejected with the thrown
- * error.
- * 3. The task function returns successfully.
- *
- * If a task function created a new task queue, the control flow will wait for
- * that queue to complete before processing the task result. If the queue
- * completes without error, the flow will settle the task's promise with the
- * value originally returned by the task function. On the other hand, if the task
- * queue terminates with an error, the task's promise will be rejected with that
- * error.
- *
- * flow.execute(function() {
- * flow.execute(() => console.log('a'));
- * flow.execute(() => console.log('b'));
- * });
- * flow.execute(() => console.log('c'));
- * // a
- * // b
- * // c
- *
- * ## ManagedPromise Integration
- *
- * In addition to the {@link ControlFlow} class, the promise module also exports
- * a [Promises/A+] {@linkplain ManagedPromise implementation} that is deeply
- * integrated with the ControlFlow. First and foremost, each promise
- * {@linkplain ManagedPromise#then() callback} is scheduled with the
- * control flow as a task. As a result, each callback is invoked in its own turn
- * of the JavaScript event loop with its own task queue. If any tasks are
- * scheduled within a callback, the callback's promised result will not be
- * settled until the task queue has completed.
- *
- * promise.fulfilled().then(function() {
- * flow.execute(function() {
- * console.log('b');
- * });
- * }).then(() => console.log('a'));
- * // b
- * // a
- *
- * ### Scheduling ManagedPromise Callbacks <a id="scheduling_callbacks"></a>
- *
- * How callbacks are scheduled in the control flow depends on when they are
- * attached to the promise. Callbacks attached to a _previously_ resolved
- * promise are immediately enqueued as subtasks of the currently running task.
- *
- * var p = promise.fulfilled();
- * flow.execute(function() {
- * flow.execute(() => console.log('A'));
- * p.then( () => console.log('B'));
- * flow.execute(() => console.log('C'));
- * p.then( () => console.log('D'));
- * }).then(function() {
- * console.log('fin');
- * });
- * // A
- * // B
- * // C
- * // D
- * // fin
- *
- * When a promise is resolved while a task function is on the call stack, any
- * callbacks also registered in that stack frame are scheduled as if the promise
- * were already resolved:
- *
- * var d = promise.defer();
- * flow.execute(function() {
- * flow.execute( () => console.log('A'));
- * d.promise.then(() => console.log('B'));
- * flow.execute( () => console.log('C'));
- * d.promise.then(() => console.log('D'));
- *
- * d.fulfill();
- * }).then(function() {
- * console.log('fin');
- * });
- * // A
- * // B
- * // C
- * // D
- * // fin
- *
- * Callbacks attached to an _unresolved_ promise within a task function are
- * only weakly scheduled as subtasks and will be dropped if they reach the
- * front of the queue before the promise is resolved. In the example below, the
- * callbacks for `B` & `D` are dropped as sub-tasks since they are attached to
- * an unresolved promise when they reach the front of the task queue.
- *
- * var d = promise.defer();
- * flow.execute(function() {
- * flow.execute( () => console.log('A'));
- * d.promise.then(() => console.log('B'));
- * flow.execute( () => console.log('C'));
- * d.promise.then(() => console.log('D'));
- *
- * setTimeout(d.fulfill, 20);
- * }).then(function() {
- * console.log('fin')
- * });
- * // A
- * // C
- * // fin
- * // B
- * // D
- *
- * If a promise is resolved while a task function is on the call stack, any
- * previously registered and unqueued callbacks (i.e. either attached while no
- * task was on the call stack, or previously dropped as described above) act as
- * _interrupts_ and are inserted at the front of the task queue. If multiple
- * promises are fulfilled, their interrupts are enqueued in the order the
- * promises are resolved.
- *
- * var d1 = promise.defer();
- * d1.promise.then(() => console.log('A'));
- *
- * var d2 = promise.defer();
- * d2.promise.then(() => console.log('B'));
- *
- * flow.execute(function() {
- * d1.promise.then(() => console.log('C'));
- * flow.execute(() => console.log('D'));
- * });
- * flow.execute(function() {
- * flow.execute(() => console.log('E'));
- * flow.execute(() => console.log('F'));
- * d1.fulfill();
- * d2.fulfill();
- * }).then(function() {
- * console.log('fin');
- * });
- * // D
- * // A
- * // C
- * // B
- * // E
- * // F
- * // fin
- *
- * Within a task function (or callback), each step of a promise chain acts as
- * an interrupt on the task queue:
- *
- * var d = promise.defer();
- * flow.execute(function() {
- * d.promise.
- * then(() => console.log('A')).
- * then(() => console.log('B')).
- * then(() => console.log('C')).
- * then(() => console.log('D'));
- *
- * flow.execute(() => console.log('E'));
- * d.fulfill();
- * }).then(function() {
- * console.log('fin');
- * });
- * // A
- * // B
- * // C
- * // D
- * // E
- * // fin
- *
- * If there are multiple promise chains derived from a single promise, they are
- * processed in the order created:
- *
- * var d = promise.defer();
- * flow.execute(function() {
- * var chain = d.promise.then(() => console.log('A'));
- *
- * chain.then(() => console.log('B')).
- * then(() => console.log('C'));
- *
- * chain.then(() => console.log('D')).
- * then(() => console.log('E'));
- *
- * flow.execute(() => console.log('F'));
- *
- * d.fulfill();
- * }).then(function() {
- * console.log('fin');
- * });
- * // A
- * // B
- * // C
- * // D
- * // E
- * // F
- * // fin
- *
- * Even though a subtask's promised result will never resolve while the task
- * function is on the stack, it will be treated as a promise resolved within the
- * task. In all other scenarios, a task's promise behaves just like a normal
- * promise. In the sample below, `C/D` is logged before `B` because the
- * resolution of `subtask1` interrupts the flow of the enclosing task. Within
- * the final subtask, `E/F` is logged in order because `subtask1` is a resolved
- * promise when that task runs.
- *
- * flow.execute(function() {
- * var subtask1 = flow.execute(() => console.log('A'));
- * var subtask2 = flow.execute(() => console.log('B'));
- *
- * subtask1.then(() => console.log('C'));
- * subtask1.then(() => console.log('D'));
- *
- * flow.execute(function() {
- * flow.execute(() => console.log('E'));
- * subtask1.then(() => console.log('F'));
- * });
- * }).then(function() {
- * console.log('fin');
- * });
- * // A
- * // C
- * // D
- * // B
- * // E
- * // F
- * // fin
- *
- * Finally, consider the following:
- *
- * var d = promise.defer();
- * d.promise.then(() => console.log('A'));
- * d.promise.then(() => console.log('B'));
- *
- * flow.execute(function() {
- * flow.execute( () => console.log('C'));
- * d.promise.then(() => console.log('D'));
- *
- * flow.execute( () => console.log('E'));
- * d.promise.then(() => console.log('F'));
- *
- * d.fulfill();
- *
- * flow.execute( () => console.log('G'));
- * d.promise.then(() => console.log('H'));
- * }).then(function() {
- * console.log('fin');
- * });
- * // A
- * // B
- * // C
- * // D
- * // E
- * // F
- * // G
- * // H
- * // fin
- *
- * In this example, callbacks are registered on `d.promise` both before and
- * during the invocation of the task function. When `d.fulfill()` is called,
- * the callbacks registered before the task (`A` & `B`) are registered as
- * interrupts. The remaining callbacks were all attached within the task and
- * are scheduled in the flow as standard tasks.
- *
- * ## Generator Support
- *
- * [Generators][GF] may be scheduled as tasks within a control flow or attached
- * as callbacks to a promise. Each time the generator yields a promise, the
- * control flow will wait for that promise to settle before executing the next
- * iteration of the generator. The yielded promise's fulfilled value will be
- * passed back into the generator:
- *
- * flow.execute(function* () {
- * var d = promise.defer();
- *
- * setTimeout(() => console.log('...waiting...'), 25);
- * setTimeout(() => d.fulfill(123), 50);
- *
- * console.log('start: ' + Date.now());
- *
- * var value = yield d.promise;
- * console.log('mid: %d; value = %d', Date.now(), value);
- *
- * yield promise.delayed(10);
- * console.log('end: ' + Date.now());
- * }).then(function() {
- * console.log('fin');
- * });
- * // start: 0
- * // ...waiting...
- * // mid: 50; value = 123
- * // end: 60
- * // fin
- *
- * Yielding the result of a promise chain will wait for the entire chain to
- * complete:
- *
- * promise.fulfilled().then(function* () {
- * console.log('start: ' + Date.now());
- *
- * var value = yield flow.
- * execute(() => console.log('A')).
- * then( () => console.log('B')).
- * then( () => 123);
- *
- * console.log('mid: %s; value = %d', Date.now(), value);
- *
- * yield flow.execute(() => console.log('C'));
- * }).then(function() {
- * console.log('fin');
- * });
- * // start: 0
- * // A
- * // B
- * // mid: 2; value = 123
- * // C
- * // fin
- *
- * Yielding a _rejected_ promise will cause the rejected value to be thrown
- * within the generator function:
- *
- * flow.execute(function* () {
- * console.log('start: ' + Date.now());
- * try {
- * yield promise.delayed(10).then(function() {
- * throw Error('boom');
- * });
- * } catch (ex) {
- * console.log('caught time: ' + Date.now());
- * console.log(ex.message);
- * }
- * });
- * // start: 0
- * // caught time: 10
- * // boom
- *
- * # Error Handling
- *
- * ES6 promises do not require users to handle a promise rejections. This can
- * result in subtle bugs as the rejections are silently "swallowed" by the
- * Promise class.
- *
- * Promise.reject(Error('boom'));
- * // ... *crickets* ...
- *
- * Selenium's promise module, on the other hand, requires that every rejection
- * be explicitly handled. When a {@linkplain ManagedPromise ManagedPromise} is
- * rejected and no callbacks are defined on that promise, it is considered an
- * _unhandled rejection_ and reported to the active task queue. If the rejection
- * remains unhandled after a single turn of the [event loop][JSEL] (scheduled
- * with a microtask), it will propagate up the stack.
- *
- * ## Error Propagation
- *
- * If an unhandled rejection occurs within a task function, that task's promised
- * result is rejected and all remaining subtasks are discarded:
- *
- * flow.execute(function() {
- * // No callbacks registered on promise -> unhandled rejection
- * promise.rejected(Error('boom'));
- * flow.execute(function() { console.log('this will never run'); });
- * }).catch(function(e) {
- * console.log(e.message);
- * });
- * // boom
- *
- * The promised results for discarded tasks are silently rejected with a
- * cancellation error and existing callback chains will never fire.
- *
- * flow.execute(function() {
- * promise.rejected(Error('boom'));
- * flow.execute(function() { console.log('a'); }).
- * then(function() { console.log('b'); });
- * }).catch(function(e) {
- * console.log(e.message);
- * });
- * // boom
- *
- * An unhandled rejection takes precedence over a task function's returned
- * result, even if that value is another promise:
- *
- * flow.execute(function() {
- * promise.rejected(Error('boom'));
- * return flow.execute(someOtherTask);
- * }).catch(function(e) {
- * console.log(e.message);
- * });
- * // boom
- *
- * If there are multiple unhandled rejections within a task, they are packaged
- * in a {@link MultipleUnhandledRejectionError}, which has an `errors` property
- * that is a `Set` of the recorded unhandled rejections:
- *
- * flow.execute(function() {
- * promise.rejected(Error('boom1'));
- * promise.rejected(Error('boom2'));
- * }).catch(function(ex) {
- * console.log(ex instanceof MultipleUnhandledRejectionError);
- * for (var e of ex.errors) {
- * console.log(e.message);
- * }
- * });
- * // boom1
- * // boom2
- *
- * When a subtask is discarded due to an unreported rejection in its parent
- * frame, the existing callbacks on that task will never settle and the
- * callbacks will not be invoked. If a new callback is attached to the subtask
- * _after_ it has been discarded, it is handled the same as adding a callback
- * to a cancelled promise: the error-callback path is invoked. This behavior is
- * intended to handle cases where the user saves a reference to a task promise,
- * as illustrated below.
- *
- * var subTask;
- * flow.execute(function() {
- * promise.rejected(Error('boom'));
- * subTask = flow.execute(function() {});
- * }).catch(function(e) {
- * console.log(e.message);
- * }).then(function() {
- * return subTask.then(
- * () => console.log('subtask success!'),
- * (e) => console.log('subtask failed:\n' + e));
- * });
- * // boom
- * // subtask failed:
- * // DiscardedTaskError: Task was discarded due to a previous failure: boom
- *
- * When a subtask fails, its promised result is treated the same as any other
- * promise: it must be handled within one turn of the rejection or the unhandled
- * rejection is propagated to the parent task. This means users can catch errors
- * from complex flows from the top level task:
- *
- * flow.execute(function() {
- * flow.execute(function() {
- * flow.execute(function() {
- * throw Error('fail!');
- * });
- * });
- * }).catch(function(e) {
- * console.log(e.message);
- * });
- * // fail!
- *
- * ## Unhandled Rejection Events
- *
- * When an unhandled rejection propagates to the root of the control flow, the
- * flow will emit an __uncaughtException__ event. If no listeners are registered
- * on the flow, the error will be rethrown to the global error handler: an
- * __uncaughtException__ event from the
- * [`process`](https://nodejs.org/api/process.html) object in node, or
- * `window.onerror` when running in a browser.
- *
- * Bottom line: you __*must*__ handle rejected promises.
- *
- * # Promises/A+ Compatibility
- *
- * This `promise` module is compliant with the [Promises/A+] specification
- * except for sections `2.2.6.1` and `2.2.6.2`:
- *
- * >
- * > - `then` may be called multiple times on the same promise.
- * > - If/when `promise` is fulfilled, all respective `onFulfilled` callbacks
- * > must execute in the order of their originating calls to `then`.
- * > - If/when `promise` is rejected, all respective `onRejected` callbacks
- * > must execute in the order of their originating calls to `then`.
- * >
- *
- * Specifically, the conformance tests contain the following scenario (for
- * brevity, only the fulfillment version is shown):
- *
- * var p1 = Promise.resolve();
- * p1.then(function() {
- * console.log('A');
- * p1.then(() => console.log('B'));
- * });
- * p1.then(() => console.log('C'));
- * // A
- * // C
- * // B
- *
- * Since the [ControlFlow](#scheduling_callbacks) executes promise callbacks as
- * tasks, with this module, the result would be:
- *
- * var p2 = promise.fulfilled();
- * p2.then(function() {
- * console.log('A');
- * p2.then(() => console.log('B');
- * });
- * p2.then(() => console.log('C'));
- * // A
- * // B
- * // C
- *
- * [JSEL]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
- * [GF]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
- * [Promises/A+]: https://promisesaplus.com/
- * [MicrotasksArticle]: https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
- */
- 'use strict';
- const error = require('./error');
- const events = require('./events');
- const logging = require('./logging');
- /**
- * Alias to help with readability and differentiate types.
- * @const
- */
- const NativePromise = Promise;
- /**
- * Whether to append traces of `then` to rejection errors.
- * @type {boolean}
- */
- var LONG_STACK_TRACES = false; // TODO: this should not be CONSTANT_CASE
- /** @const */
- const LOG = logging.getLogger('promise');
- const UNIQUE_IDS = new WeakMap;
- let nextId = 1;
- function getUid(obj) {
- let id = UNIQUE_IDS.get(obj);
- if (!id) {
- id = nextId;
- nextId += 1;
- UNIQUE_IDS.set(obj, id);
- }
- return id;
- }
- /**
- * Runs the given function after a microtask yield.
- * @param {function()} fn The function to run.
- */
- function asyncRun(fn) {
- NativePromise.resolve().then(function() {
- try {
- fn();
- } catch (ignored) {
- // Do nothing.
- }
- });
- }
- /**
- * @param {number} level What level of verbosity to log with.
- * @param {(string|function(this: T): string)} loggable The message to log.
- * @param {T=} opt_self The object in whose context to run the loggable
- * function.
- * @template T
- */
- function vlog(level, loggable, opt_self) {
- var logLevel = logging.Level.FINE;
- if (level > 1) {
- logLevel = logging.Level.FINEST;
- } else if (level > 0) {
- logLevel = logging.Level.FINER;
- }
- if (typeof loggable === 'function') {
- loggable = loggable.bind(opt_self);
- }
- LOG.log(logLevel, loggable);
- }
- /**
- * Generates an error to capture the current stack trace.
- * @param {string} name Error name for this stack trace.
- * @param {string} msg Message to record.
- * @param {Function=} opt_topFn The function that should appear at the top of
- * the stack; only applicable in V8.
- * @return {!Error} The generated error.
- */
- function captureStackTrace(name, msg, opt_topFn) {
- var e = Error(msg);
- e.name = name;
- if (Error.captureStackTrace) {
- Error.captureStackTrace(e, opt_topFn);
- } else {
- var stack = Error().stack;
- if (stack) {
- e.stack = e.toString();
- e.stack += '\n' + stack;
- }
- }
- return e;
- }
- /**
- * Error used when the computation of a promise is cancelled.
- */
- class CancellationError extends Error {
- /**
- * @param {string=} opt_msg The cancellation message.
- */
- constructor(opt_msg) {
- super(opt_msg);
- /** @override */
- this.name = this.constructor.name;
- /** @private {boolean} */
- this.silent_ = false;
- }
- /**
- * Wraps the given error in a CancellationError.
- *
- * @param {*} error The error to wrap.
- * @param {string=} opt_msg The prefix message to use.
- * @return {!CancellationError} A cancellation error.
- */
- static wrap(error, opt_msg) {
- var message;
- if (error instanceof CancellationError) {
- return new CancellationError(
- opt_msg ? (opt_msg + ': ' + error.message) : error.message);
- } else if (opt_msg) {
- message = opt_msg;
- if (error) {
- message += ': ' + error;
- }
- return new CancellationError(message);
- }
- if (error) {
- message = error + '';
- }
- return new CancellationError(message);
- }
- }
- /**
- * Error used to cancel tasks when a control flow is reset.
- * @final
- */
- class FlowResetError extends CancellationError {
- constructor() {
- super('ControlFlow was reset');
- this.silent_ = true;
- }
- }
- /**
- * Error used to cancel tasks that have been discarded due to an uncaught error
- * reported earlier in the control flow.
- * @final
- */
- class DiscardedTaskError extends CancellationError {
- /** @param {*} error The original error. */
- constructor(error) {
- if (error instanceof DiscardedTaskError) {
- return /** @type {!DiscardedTaskError} */(error);
- }
- var msg = '';
- if (error) {
- msg = ': ' + (
- typeof error.message === 'string' ? error.message : error);
- }
- super('Task was discarded due to a previous failure' + msg);
- this.silent_ = true;
- }
- }
- /**
- * Error used when there are multiple unhandled promise rejections detected
- * within a task or callback.
- *
- * @final
- */
- class MultipleUnhandledRejectionError extends Error {
- /**
- * @param {!(Set<*>)} errors The errors to report.
- */
- constructor(errors) {
- super('Multiple unhandled promise rejections reported');
- /** @override */
- this.name = this.constructor.name;
- /** @type {!Set<*>} */
- this.errors = errors;
- }
- }
- /**
- * Property used to flag constructor's as implementing the Thenable interface
- * for runtime type checking.
- * @const
- */
- const IMPLEMENTED_BY_SYMBOL = Symbol('promise.Thenable');
- const CANCELLABLE_SYMBOL = Symbol('promise.CancellableThenable');
- /**
- * @param {function(new: ?)} ctor
- * @param {!Object} symbol
- */
- function addMarkerSymbol(ctor, symbol) {
- try {
- ctor.prototype[symbol] = true;
- } catch (ignored) {
- // Property access denied?
- }
- }
- /**
- * @param {*} object
- * @param {!Object} symbol
- * @return {boolean}
- */
- function hasMarkerSymbol(object, symbol) {
- if (!object) {
- return false;
- }
- try {
- return !!object[symbol];
- } catch (e) {
- return false; // Property access seems to be forbidden.
- }
- }
- /**
- * Thenable is a promise-like object with a {@code then} method which may be
- * used to schedule callbacks on a promised value.
- *
- * @record
- * @extends {IThenable<T>}
- * @template T
- */
- class Thenable {
- /**
- * Adds a property to a class prototype to allow runtime checks of whether
- * instances of that class implement the Thenable interface.
- * @param {function(new: Thenable, ...?)} ctor The
- * constructor whose prototype to modify.
- */
- static addImplementation(ctor) {
- addMarkerSymbol(ctor, IMPLEMENTED_BY_SYMBOL);
- }
- /**
- * Checks if an object has been tagged for implementing the Thenable
- * interface as defined by {@link Thenable.addImplementation}.
- * @param {*} object The object to test.
- * @return {boolean} Whether the object is an implementation of the Thenable
- * interface.
- */
- static isImplementation(object) {
- return hasMarkerSymbol(object, IMPLEMENTED_BY_SYMBOL);
- }
- /**
- * Registers listeners for when this instance is resolved.
- *
- * @param {?(function(T): (R|IThenable<R>))=} opt_callback The
- * function to call if this promise is successfully resolved. The function
- * should expect a single argument: the promise's resolved value.
- * @param {?(function(*): (R|IThenable<R>))=} opt_errback
- * The function to call if this promise is rejected. The function should
- * expect a single argument: the rejection reason.
- * @return {!Thenable<R>} A new promise which will be resolved with the result
- * of the invoked callback.
- * @template R
- */
- then(opt_callback, opt_errback) {}
- /**
- * Registers a listener for when this promise is rejected. This is synonymous
- * with the {@code catch} clause in a synchronous API:
- *
- * // Synchronous API:
- * try {
- * doSynchronousWork();
- * } catch (ex) {
- * console.error(ex);
- * }
- *
- * // Asynchronous promise API:
- * doAsynchronousWork().catch(function(ex) {
- * console.error(ex);
- * });
- *
- * @param {function(*): (R|IThenable<R>)} errback The
- * function to call if this promise is rejected. The function should
- * expect a single argument: the rejection reason.
- * @return {!Thenable<R>} A new promise which will be resolved with the result
- * of the invoked callback.
- * @template R
- */
- catch(errback) {}
- }
- /**
- * Marker interface for objects that allow consumers to request the cancellation
- * of a promise-based operation. A cancelled promise will be rejected with a
- * {@link CancellationError}.
- *
- * This interface is considered package-private and should not be used outside
- * of selenium-webdriver.
- *
- * @interface
- * @extends {Thenable<T>}
- * @template T
- * @package
- */
- class CancellableThenable {
- /**
- * @param {function(new: CancellableThenable, ...?)} ctor
- */
- static addImplementation(ctor) {
- Thenable.addImplementation(ctor);
- addMarkerSymbol(ctor, CANCELLABLE_SYMBOL);
- }
- /**
- * @param {*} object
- * @return {boolean}
- */
- static isImplementation(object) {
- return hasMarkerSymbol(object, CANCELLABLE_SYMBOL);
- }
- /**
- * Requests the cancellation of the computation of this promise's value,
- * rejecting the promise in the process. This method is a no-op if the promise
- * has already been resolved.
- *
- * @param {(string|Error)=} opt_reason The reason this promise is being
- * cancelled. This value will be wrapped in a {@link CancellationError}.
- */
- cancel(opt_reason) {}
- }
- /**
- * @enum {string}
- */
- const PromiseState = {
- PENDING: 'pending',
- BLOCKED: 'blocked',
- REJECTED: 'rejected',
- FULFILLED: 'fulfilled'
- };
- /**
- * Internal map used to store cancellation handlers for {@link ManagedPromise}
- * objects. This is an internal implementation detail used by the
- * {@link TaskQueue} class to monitor for when a promise is cancelled without
- * generating an extra promise via then().
- *
- * @const {!WeakMap<!ManagedPromise, function(!CancellationError)>}
- */
- const ON_CANCEL_HANDLER = new WeakMap;
- const SKIP_LOG = Symbol('skip-log');
- const FLOW_LOG = logging.getLogger('promise.ControlFlow');
- /**
- * Represents the eventual value of a completed operation. Each promise may be
- * in one of three states: pending, fulfilled, or rejected. Each promise starts
- * in the pending state and may make a single transition to either a
- * fulfilled or rejected state, at which point the promise is considered
- * resolved.
- *
- * @implements {CancellableThenable<T>}
- * @template T
- * @see http://promises-aplus.github.io/promises-spec/
- */
- class ManagedPromise {
- /**
- * @param {function(
- * function((T|IThenable<T>|Thenable)=),
- * function(*=))} resolver
- * Function that is invoked immediately to begin computation of this
- * promise's value. The function should accept a pair of callback
- * functions, one for fulfilling the promise and another for rejecting it.
- * @param {ControlFlow=} opt_flow The control flow
- * this instance was created under. Defaults to the currently active flow.
- * @param {?=} opt_skipLog An internal parameter used to skip logging the
- * creation of this promise. This parameter has no effect unless it is
- * strictly equal to an internal symbol. In other words, this parameter
- * is always ignored for external code.
- */
- constructor(resolver, opt_flow, opt_skipLog) {
- if (!usePromiseManager()) {
- throw TypeError(
- 'Unable to create a managed promise instance: the promise manager has'
- + ' been disabled by the SELENIUM_PROMISE_MANAGER environment'
- + ' variable: ' + process.env['SELENIUM_PROMISE_MANAGER']);
- } else if (opt_skipLog !== SKIP_LOG) {
- FLOW_LOG.warning(() => {
- let e =
- captureStackTrace(
- 'ManagedPromiseError',
- 'Creating a new managed Promise. This call will fail when the'
- + ' promise manager is disabled',
- ManagedPromise)
- return e.stack;
- });
- }
- getUid(this);
- /** @private {!ControlFlow} */
- this.flow_ = opt_flow || controlFlow();
- /** @private {Error} */
- this.stack_ = null;
- if (LONG_STACK_TRACES) {
- this.stack_ = captureStackTrace('ManagedPromise', 'new', this.constructor);
- }
- /** @private {Thenable<?>} */
- this.parent_ = null;
- /** @private {Array<!Task>} */
- this.callbacks_ = null;
- /** @private {PromiseState} */
- this.state_ = PromiseState.PENDING;
- /** @private {boolean} */
- this.handled_ = false;
- /** @private {*} */
- this.value_ = undefined;
- /** @private {TaskQueue} */
- this.queue_ = null;
- try {
- var self = this;
- resolver(function(value) {
- self.resolve_(PromiseState.FULFILLED, value);
- }, function(reason) {
- self.resolve_(PromiseState.REJECTED, reason);
- });
- } catch (ex) {
- this.resolve_(PromiseState.REJECTED, ex);
- }
- }
- /**
- * Creates a promise that is immediately resolved with the given value.
- *
- * @param {T=} opt_value The value to resolve.
- * @return {!ManagedPromise<T>} A promise resolved with the given value.
- * @template T
- */
- static resolve(opt_value) {
- if (opt_value instanceof ManagedPromise) {
- return opt_value;
- }
- return new ManagedPromise(resolve => resolve(opt_value));
- }
- /**
- * Creates a promise that is immediately rejected with the given reason.
- *
- * @param {*=} opt_reason The rejection reason.
- * @return {!ManagedPromise<?>} A new rejected promise.
- */
- static reject(opt_reason) {
- return new ManagedPromise((_, reject) => reject(opt_reason));
- }
- /** @override */
- toString() {
- return 'ManagedPromise::' + getUid(this) +
- ' {[[PromiseStatus]]: "' + this.state_ + '"}';
- }
- /**
- * Resolves this promise. If the new value is itself a promise, this function
- * will wait for it to be resolved before notifying the registered listeners.
- * @param {PromiseState} newState The promise's new state.
- * @param {*} newValue The promise's new value.
- * @throws {TypeError} If {@code newValue === this}.
- * @private
- */
- resolve_(newState, newValue) {
- if (PromiseState.PENDING !== this.state_) {
- return;
- }
- if (newValue === this) {
- // See promise a+, 2.3.1
- // http://promises-aplus.github.io/promises-spec/#point-48
- newValue = new TypeError('A promise may not resolve to itself');
- newState = PromiseState.REJECTED;
- }
- this.parent_ = null;
- this.state_ = PromiseState.BLOCKED;
- if (newState !== PromiseState.REJECTED) {
- if (Thenable.isImplementation(newValue)) {
- // 2.3.2
- newValue = /** @type {!Thenable} */(newValue);
- this.parent_ = newValue;
- newValue.then(
- this.unblockAndResolve_.bind(this, PromiseState.FULFILLED),
- this.unblockAndResolve_.bind(this, PromiseState.REJECTED));
- return;
- } else if (newValue
- && (typeof newValue === 'object' || typeof newValue === 'function')) {
- // 2.3.3
- try {
- // 2.3.3.1
- var then = newValue['then'];
- } catch (e) {
- // 2.3.3.2
- this.state_ = PromiseState.REJECTED;
- this.value_ = e;
- this.scheduleNotifications_();
- return;
- }
- if (typeof then === 'function') {
- // 2.3.3.3
- this.invokeThen_(/** @type {!Object} */(newValue), then);
- return;
- }
- }
- }
- if (newState === PromiseState.REJECTED &&
- isError(newValue) && newValue.stack && this.stack_) {
- newValue.stack += '\nFrom: ' + (this.stack_.stack || this.stack_);
- }
- // 2.3.3.4 and 2.3.4
- this.state_ = newState;
- this.value_ = newValue;
- this.scheduleNotifications_();
- }
- /**
- * Invokes a thenable's "then" method according to 2.3.3.3 of the promise
- * A+ spec.
- * @param {!Object} x The thenable object.
- * @param {!Function} then The "then" function to invoke.
- * @private
- */
- invokeThen_(x, then) {
- var called = false;
- var self = this;
- var resolvePromise = function(value) {
- if (!called) { // 2.3.3.3.3
- called = true;
- // 2.3.3.3.1
- self.unblockAndResolve_(PromiseState.FULFILLED, value);
- }
- };
- var rejectPromise = function(reason) {
- if (!called) { // 2.3.3.3.3
- called = true;
- // 2.3.3.3.2
- self.unblockAndResolve_(PromiseState.REJECTED, reason);
- }
- };
- try {
- // 2.3.3.3
- then.call(x, resolvePromise, rejectPromise);
- } catch (e) {
- // 2.3.3.3.4.2
- rejectPromise(e);
- }
- }
- /**
- * @param {PromiseState} newState The promise's new state.
- * @param {*} newValue The promise's new value.
- * @private
- */
- unblockAndResolve_(newState, newValue) {
- if (this.state_ === PromiseState.BLOCKED) {
- this.state_ = PromiseState.PENDING;
- this.resolve_(newState, newValue);
- }
- }
- /**
- * @private
- */
- scheduleNotifications_() {
- vlog(2, () => this + ' scheduling notifications', this);
- ON_CANCEL_HANDLER.delete(this);
- if (this.value_ instanceof CancellationError
- && this.value_.silent_) {
- this.callbacks_ = null;
- }
- if (!this.queue_) {
- this.queue_ = this.flow_.getActiveQueue_();
- }
- if (!this.handled_ &&
- this.state_ === PromiseState.REJECTED &&
- !(this.value_ instanceof CancellationError)) {
- this.queue_.addUnhandledRejection(this);
- }
- this.queue_.scheduleCallbacks(this);
- }
- /** @override */
- cancel(opt_reason) {
- if (!canCancel(this)) {
- return;
- }
- if (this.parent_ && canCancel(this.parent_)) {
- /** @type {!CancellableThenable} */(this.parent_).cancel(opt_reason);
- } else {
- var reason = CancellationError.wrap(opt_reason);
- let onCancel = ON_CANCEL_HANDLER.get(this);
- if (onCancel) {
- onCancel(reason);
- ON_CANCEL_HANDLER.delete(this);
- }
- if (this.state_ === PromiseState.BLOCKED) {
- this.unblockAndResolve_(PromiseState.REJECTED, reason);
- } else {
- this.resolve_(PromiseState.REJECTED, reason);
- }
- }
- function canCancel(promise) {
- if (!(promise instanceof ManagedPromise)) {
- return CancellableThenable.isImplementation(promise);
- }
- return promise.state_ === PromiseState.PENDING
- || promise.state_ === PromiseState.BLOCKED;
- }
- }
- /** @override */
- then(opt_callback, opt_errback) {
- return this.addCallback_(
- opt_callback, opt_errback, 'then', ManagedPromise.prototype.then);
- }
- /** @override */
- catch(errback) {
- return this.addCallback_(
- null, errback, 'catch', ManagedPromise.prototype.catch);
- }
- /**
- * @param {function(): (R|IThenable<R>)} callback
- * @return {!ManagedPromise<R>}
- * @template R
- * @see ./promise.finally()
- */
- finally(callback) {
- let result = thenFinally(this, callback);
- return /** @type {!ManagedPromise} */(result);
- }
- /**
- * Registers a new callback with this promise
- * @param {(function(T): (R|IThenable<R>)|null|undefined)} callback The
- * fulfillment callback.
- * @param {(function(*): (R|IThenable<R>)|null|undefined)} errback The
- * rejection callback.
- * @param {string} name The callback name.
- * @param {!Function} fn The function to use as the top of the stack when
- * recording the callback's creation point.
- * @return {!ManagedPromise<R>} A new promise which will be resolved with the
- * result of the invoked callback.
- * @template R
- * @private
- */
- addCallback_(callback, errback, name, fn) {
- if (typeof callback !== 'function' && typeof errback !== 'function') {
- return this;
- }
- this.handled_ = true;
- if (this.queue_) {
- this.queue_.clearUnhandledRejection(this);
- }
- var cb = new Task(
- this.flow_,
- this.invokeCallback_.bind(this, callback, errback),
- name,
- LONG_STACK_TRACES ? {name: 'Promise', top: fn} : undefined);
- cb.promise.parent_ = this;
- if (this.state_ !== PromiseState.PENDING &&
- this.state_ !== PromiseState.BLOCKED) {
- this.flow_.getActiveQueue_().enqueue(cb);
- } else {
- if (!this.callbacks_) {
- this.callbacks_ = [];
- }
- this.callbacks_.push(cb);
- cb.blocked = true;
- this.flow_.getActiveQueue_().enqueue(cb);
- }
- return cb.promise;
- }
- /**
- * Invokes a callback function attached to this promise.
- * @param {(function(T): (R|IThenable<R>)|null|undefined)} callback The
- * fulfillment callback.
- * @param {(function(*): (R|IThenable<R>)|null|undefined)} errback The
- * rejection callback.
- * @template R
- * @private
- */
- invokeCallback_(callback, errback) {
- var callbackFn = callback;
- if (this.state_ === PromiseState.REJECTED) {
- callbackFn = errback;
- }
- if (typeof callbackFn === 'function') {
- if (isGenerator(callbackFn)) {
- return consume(callbackFn, null, this.value_);
- }
- return callbackFn(this.value_);
- } else if (this.state_ === PromiseState.REJECTED) {
- throw this.value_;
- } else {
- return this.value_;
- }
- }
- }
- CancellableThenable.addImplementation(ManagedPromise);
- /**
- * @param {!ManagedPromise} promise
- * @return {boolean}
- */
- function isPending(promise) {
- return promise.state_ === PromiseState.PENDING;
- }
- /**
- * Structural interface for a deferred promise resolver.
- * @record
- * @template T
- */
- function Resolver() {}
- /**
- * The promised value for this resolver.
- * @type {!Thenable<T>}
- */
- Resolver.prototype.promise;
- /**
- * Resolves the promised value with the given `value`.
- * @param {T|Thenable<T>} value
- * @return {void}
- */
- Resolver.prototype.resolve;
- /**
- * Rejects the promised value with the given `reason`.
- * @param {*} reason
- * @return {void}
- */
- Resolver.prototype.reject;
- /**
- * Represents a value that will be resolved at some point in the future. This
- * class represents the protected "producer" half of a ManagedPromise - each Deferred
- * has a {@code promise} property that may be returned to consumers for
- * registering callbacks, reserving the ability to resolve the deferred to the
- * producer.
- *
- * If this Deferred is rejected and there are no listeners registered before
- * the next turn of the event loop, the rejection will be passed to the
- * {@link ControlFlow} as an unhandled failure.
- *
- * @template T
- * @implements {Resolver<T>}
- */
- class Deferred {
- /**
- * @param {ControlFlow=} opt_flow The control flow this instance was
- * created under. This should only be provided during unit tests.
- * @param {?=} opt_skipLog An internal parameter used to skip logging the
- * creation of this promise. This parameter has no effect unless it is
- * strictly equal to an internal symbol. In other words, this parameter
- * is always ignored for external code.
- */
- constructor(opt_flow, opt_skipLog) {
- var fulfill, reject;
- /** @type {!ManagedPromise<T>} */
- this.promise = new ManagedPromise(function(f, r) {
- fulfill = f;
- reject = r;
- }, opt_flow, opt_skipLog);
- var self = this;
- var checkNotSelf = function(value) {
- if (value === self) {
- throw new TypeError('May not resolve a Deferred with itself');
- }
- };
- /**
- * Resolves this deferred with the given value. It is safe to call this as a
- * normal function (with no bound "this").
- * @param {(T|IThenable<T>|Thenable)=} opt_value The fulfilled value.
- * @const
- */
- this.resolve = function(opt_value) {
- checkNotSelf(opt_value);
- fulfill(opt_value);
- };
- /**
- * An alias for {@link #resolve}.
- * @const
- */
- this.fulfill = this.resolve;
- /**
- * Rejects this promise with the given reason. It is safe to call this as a
- * normal function (with no bound "this").
- * @param {*=} opt_reason The rejection reason.
- * @const
- */
- this.reject = function(opt_reason) {
- checkNotSelf(opt_reason);
- reject(opt_reason);
- };
- }
- }
- /**
- * Tests if a value is an Error-like object. This is more than an straight
- * instanceof check since the value may originate from another context.
- * @param {*} value The value to test.
- * @return {boolean} Whether the value is an error.
- */
- function isError(value) {
- return value instanceof Error ||
- (!!value && typeof value === 'object'
- && typeof value.message === 'string');
- }
- /**
- * Determines whether a {@code value} should be treated as a promise.
- * Any object whose "then" property is a function will be considered a promise.
- *
- * @param {?} value The value to test.
- * @return {boolean} Whether the value is a promise.
- */
- function isPromise(value) {
- try {
- // Use array notation so the Closure compiler does not obfuscate away our
- // contract.
- return value
- && (typeof value === 'object' || typeof value === 'function')
- && typeof value['then'] === 'function';
- } catch (ex) {
- return false;
- }
- }
- /**
- * Creates a promise that will be resolved at a set time in the future.
- * @param {number} ms The amount of time, in milliseconds, to wait before
- * resolving the promise.
- * @return {!Thenable} The promise.
- */
- function delayed(ms) {
- return createPromise(resolve => {
- setTimeout(() => resolve(), ms);
- });
- }
- /**
- * Creates a new deferred resolver.
- *
- * If the promise manager is currently enabled, this function will return a
- * {@link Deferred} instance. Otherwise, it will return a resolver for a
- * {@linkplain NativePromise native promise}.
- *
- * @return {!Resolver<T>} A new deferred resolver.
- * @template T
- */
- function defer() {
- if (usePromiseManager()) {
- return new Deferred();
- }
- let resolve, reject;
- let promise = new NativePromise((_resolve, _reject) => {
- resolve = _resolve;
- reject = _reject;
- });
- return {promise, resolve, reject};
- }
- /**
- * Creates a promise that has been resolved with the given value.
- *
- * If the promise manager is currently enabled, this function will return a
- * {@linkplain ManagedPromise managed promise}. Otherwise, it will return a
- * {@linkplain NativePromise native promise}.
- *
- * @param {T=} opt_value The resolved value.
- * @return {!Thenable<T>} The resolved promise.
- * @template T
- */
- function fulfilled(opt_value) {
- let ctor = usePromiseManager() ? ManagedPromise : NativePromise;
- if (opt_value instanceof ctor) {
- return /** @type {!Thenable} */(opt_value);
- }
- if (usePromiseManager()) {
- // We can skip logging warnings about creating a managed promise because
- // this function will automatically switch to use a native promise when
- // the promise manager is disabled.
- return new ManagedPromise(
- resolve => resolve(opt_value), undefined, SKIP_LOG);
- }
- return NativePromise.resolve(opt_value);
- }
- /**
- * Creates a promise that has been rejected with the given reason.
- *
- * If the promise manager is currently enabled, this function will return a
- * {@linkplain ManagedPromise managed promise}. Otherwise, it will return a
- * {@linkplain NativePromise native promise}.
- *
- * @param {*=} opt_reason The rejection reason; may be any value, but is
- * usually an Error or a string.
- * @return {!Thenable<?>} The rejected promise.
- */
- function rejected(opt_reason) {
- if (usePromiseManager()) {
- // We can skip logging warnings about creating a managed promise because
- // this function will automatically switch to use a native promise when
- // the promise manager is disabled.
- return new ManagedPromise(
- (_, reject) => reject(opt_reason), undefined, SKIP_LOG);
- }
- return NativePromise.reject(opt_reason);
- }
- /**
- * Wraps a function that expects a node-style callback as its final
- * argument. This callback expects two arguments: an error value (which will be
- * null if the call succeeded), and the success value as the second argument.
- * The callback will the resolve or reject the returned promise, based on its
- * arguments.
- * @param {!Function} fn The function to wrap.
- * @param {...?} var_args The arguments to apply to the function, excluding the
- * final callback.
- * @return {!Thenable} A promise that will be resolved with the
- * result of the provided function's callback.
- */
- function checkedNodeCall(fn, var_args) {
- let args = Array.prototype.slice.call(arguments, 1);
- return createPromise(function(fulfill, reject) {
- try {
- args.push(function(error, value) {
- error ? reject(error) : fulfill(value);
- });
- fn.apply(undefined, args);
- } catch (ex) {
- reject(ex);
- }
- });
- }
- /**
- * Registers a listener to invoke when a promise is resolved, regardless
- * of whether the promise's value was successfully computed. This function
- * is synonymous with the {@code finally} clause in a synchronous API:
- *
- * // Synchronous API:
- * try {
- * doSynchronousWork();
- * } finally {
- * cleanUp();
- * }
- *
- * // Asynchronous promise API:
- * doAsynchronousWork().finally(cleanUp);
- *
- * __Note:__ similar to the {@code finally} clause, if the registered
- * callback returns a rejected promise or throws an error, it will silently
- * replace the rejection error (if any) from this promise:
- *
- * try {
- * throw Error('one');
- * } finally {
- * throw Error('two'); // Hides Error: one
- * }
- *
- * let p = Promise.reject(Error('one'));
- * promise.finally(p, function() {
- * throw Error('two'); // Hides Error: one
- * });
- *
- * @param {!IThenable<?>} promise The promise to add the listener to.
- * @param {function(): (R|IThenable<R>)} callback The function to call when
- * the promise is resolved.
- * @return {!IThenable<R>} A promise that will be resolved with the callback
- * result.
- * @template R
- */
- function thenFinally(promise, callback) {
- let error;
- let mustThrow = false;
- return promise.then(function() {
- return callback();
- }, function(err) {
- error = err;
- mustThrow = true;
- return callback();
- }).then(function() {
- if (mustThrow) {
- throw error;
- }
- });
- }
- /**
- * Registers an observer on a promised {@code value}, returning a new promise
- * that will be resolved when the value is. If {@code value} is not a promise,
- * then the return promise will be immediately resolved.
- * @param {*} value The value to observe.
- * @param {Function=} opt_callback The function to call when the value is
- * resolved successfully.
- * @param {Function=} opt_errback The function to call when the value is
- * rejected.
- * @return {!Thenable} A new promise.
- * @deprecated Use `promise.fulfilled(value).then(opt_callback, opt_errback)`
- */
- function when(value, opt_callback, opt_errback) {
- return fulfilled(value).then(opt_callback, opt_errback);
- }
- /**
- * Invokes the appropriate callback function as soon as a promised `value` is
- * resolved.
- *
- * @param {*} value The value to observe.
- * @param {Function} callback The function to call when the value is
- * resolved successfully.
- * @param {Function=} opt_errback The function to call when the value is
- * rejected.
- */
- function asap(value, callback, opt_errback) {
- if (isPromise(value)) {
- value.then(callback, opt_errback);
- } else if (callback) {
- callback(value);
- }
- }
- /**
- * Given an array of promises, will return a promise that will be fulfilled
- * with the fulfillment values of the input array's values. If any of the
- * input array's promises are rejected, the returned promise will be rejected
- * with the same reason.
- *
- * @param {!Array<(T|!ManagedPromise<T>)>} arr An array of
- * promises to wait on.
- * @return {!Thenable<!Array<T>>} A promise that is
- * fulfilled with an array containing the fulfilled values of the
- * input array, or rejected with the same reason as the first
- * rejected value.
- * @template T
- */
- function all(arr) {
- return createPromise(function(fulfill, reject) {
- var n = arr.length;
- var values = [];
- if (!n) {
- fulfill(values);
- return;
- }
- var toFulfill = n;
- var onFulfilled = function(index, value) {
- values[index] = value;
- toFulfill--;
- if (toFulfill == 0) {
- fulfill(values);
- }
- };
- function processPromise(index) {
- asap(arr[index], function(value) {
- onFulfilled(index, value);
- }, reject);
- }
- for (var i = 0; i < n; ++i) {
- processPromise(i);
- }
- });
- }
- /**
- * Calls a function for each element in an array and inserts the result into a
- * new array, which is used as the fulfillment value of the promise returned
- * by this function.
- *
- * If the return value of the mapping function is a promise, this function
- * will wait for it to be fulfilled before inserting it into the new array.
- *
- * If the mapping function throws or returns a rejected promise, the
- * promise returned by this function will be rejected with the same reason.
- * Only the first failure will be reported; all subsequent errors will be
- * silently ignored.
- *
- * @param {!(Array<TYPE>|ManagedPromise<!Array<TYPE>>)} arr The
- * array to iterator over, or a promise that will resolve to said array.
- * @param {function(this: SELF, TYPE, number, !Array<TYPE>): ?} fn The
- * function to call for each element in the array. This function should
- * expect three arguments (the element, the index, and the array itself.
- * @param {SELF=} opt_self The object to be used as the value of 'this' within
- * {@code fn}.
- * @template TYPE, SELF
- */
- function map(arr, fn, opt_self) {
- return createPromise(resolve => resolve(arr)).then(v => {
- if (!Array.isArray(v)) {
- throw TypeError('not an array');
- }
- var arr = /** @type {!Array} */(v);
- return createPromise(function(fulfill, reject) {
- var n = arr.length;
- var values = new Array(n);
- (function processNext(i) {
- for (; i < n; i++) {
- if (i in arr) {
- break;
- }
- }
- if (i >= n) {
- fulfill(values);
- return;
- }
- try {
- asap(
- fn.call(opt_self, arr[i], i, /** @type {!Array} */(arr)),
- function(value) {
- values[i] = value;
- processNext(i + 1);
- },
- reject);
- } catch (ex) {
- reject(ex);
- }
- })(0);
- });
- });
- }
- /**
- * Calls a function for each element in an array, and if the function returns
- * true adds the element to a new array.
- *
- * If the return value of the filter function is a promise, this function
- * will wait for it to be fulfilled before determining whether to insert the
- * element into the new array.
- *
- * If the filter function throws or returns a rejected promise, the promise
- * returned by this function will be rejected with the same reason. Only the
- * first failure will be reported; all subsequent errors will be silently
- * ignored.
- *
- * @param {!(Array<TYPE>|ManagedPromise<!Array<TYPE>>)} arr The
- * array to iterator over, or a promise that will resolve to said array.
- * @param {function(this: SELF, TYPE, number, !Array<TYPE>): (
- * boolean|ManagedPromise<boolean>)} fn The function
- * to call for each element in the array.
- * @param {SELF=} opt_self The object to be used as the value of 'this' within
- * {@code fn}.
- * @template TYPE, SELF
- */
- function filter(arr, fn, opt_self) {
- return createPromise(resolve => resolve(arr)).then(v => {
- if (!Array.isArray(v)) {
- throw TypeError('not an array');
- }
- var arr = /** @type {!Array} */(v);
- return createPromise(function(fulfill, reject) {
- var n = arr.length;
- var values = [];
- var valuesLength = 0;
- (function processNext(i) {
- for (; i < n; i++) {
- if (i in arr) {
- break;
- }
- }
- if (i >= n) {
- fulfill(values);
- return;
- }
- try {
- var value = arr[i];
- var include = fn.call(opt_self, value, i, /** @type {!Array} */(arr));
- asap(include, function(include) {
- if (include) {
- values[valuesLength++] = value;
- }
- processNext(i + 1);
- }, reject);
- } catch (ex) {
- reject(ex);
- }
- })(0);
- });
- });
- }
- /**
- * Returns a promise that will be resolved with the input value in a
- * fully-resolved state. If the value is an array, each element will be fully
- * resolved. Likewise, if the value is an object, all keys will be fully
- * resolved. In both cases, all nested arrays and objects will also be
- * fully resolved. All fields are resolved in place; the returned promise will
- * resolve on {@code value} and not a copy.
- *
- * Warning: This function makes no checks against objects that contain
- * cyclical references:
- *
- * var value = {};
- * value['self'] = value;
- * promise.fullyResolved(value); // Stack overflow.
- *
- * @param {*} value The value to fully resolve.
- * @return {!Thenable} A promise for a fully resolved version
- * of the input value.
- */
- function fullyResolved(value) {
- if (isPromise(value)) {
- return fulfilled(value).then(fullyResolveValue);
- }
- return fullyResolveValue(value);
- }
- /**
- * @param {*} value The value to fully resolve. If a promise, assumed to
- * already be resolved.
- * @return {!Thenable} A promise for a fully resolved version
- * of the input value.
- */
- function fullyResolveValue(value) {
- if (Array.isArray(value)) {
- return fullyResolveKeys(/** @type {!Array} */ (value));
- }
- if (isPromise(value)) {
- if (isPromise(value)) {
- // We get here when the original input value is a promise that
- // resolves to itself. When the user provides us with such a promise,
- // trust that it counts as a "fully resolved" value and return it.
- // Of course, since it's already a promise, we can just return it
- // to the user instead of wrapping it in another promise.
- return /** @type {!ManagedPromise} */ (value);
- }
- }
- if (value && typeof value === 'object') {
- return fullyResolveKeys(/** @type {!Object} */ (value));
- }
- if (typeof value === 'function') {
- return fullyResolveKeys(/** @type {!Object} */ (value));
- }
- return createPromise(resolve => resolve(value));
- }
- /**
- * @param {!(Array|Object)} obj the object to resolve.
- * @return {!Thenable} A promise that will be resolved with the
- * input object once all of its values have been fully resolved.
- */
- function fullyResolveKeys(obj) {
- var isArray = Array.isArray(obj);
- var numKeys = isArray ? obj.length : (function() {
- let n = 0;
- for (let key in obj) {
- n += 1;
- }
- return n;
- })();
- if (!numKeys) {
- return createPromise(resolve => resolve(obj));
- }
- function forEachProperty(obj, fn) {
- for (let key in obj) {
- fn.call(null, obj[key], key, obj);
- }
- }
- function forEachElement(arr, fn) {
- arr.forEach(fn);
- }
- var numResolved = 0;
- return createPromise(function(fulfill, reject) {
- var forEachKey = isArray ? forEachElement: forEachProperty;
- forEachKey(obj, function(partialValue, key) {
- if (!Array.isArray(partialValue)
- && (!partialValue || typeof partialValue !== 'object')) {
- maybeResolveValue();
- return;
- }
- fullyResolved(partialValue).then(
- function(resolvedValue) {
- obj[key] = resolvedValue;
- maybeResolveValue();
- },
- reject);
- });
- function maybeResolveValue() {
- if (++numResolved == numKeys) {
- fulfill(obj);
- }
- }
- });
- }
- //////////////////////////////////////////////////////////////////////////////
- //
- // ControlFlow
- //
- //////////////////////////////////////////////////////////////////////////////
- /**
- * Defines methods for coordinating the execution of asynchronous tasks.
- * @record
- */
- class Scheduler {
- /**
- * Schedules a task for execution. If the task function is a generator, the
- * task will be executed using {@link ./promise.consume consume()}.
- *
- * @param {function(): (T|IThenable<T>)} fn The function to call to start the
- * task.
- * @param {string=} opt_description A description of the task for debugging
- * purposes.
- * @return {!Thenable<T>} A promise that will be resolved with the task
- * result.
- * @template T
- */
- execute(fn, opt_description) {}
- /**
- * Creates a new promise using the given resolver function.
- *
- * @param {function(
- * function((T|IThenable<T>|Thenable|null)=),
- * function(*=))} resolver
- * @return {!Thenable<T>}
- * @template T
- */
- promise(resolver) {}
- /**
- * Schedules a `setTimeout` call.
- *
- * @param {number} ms The timeout delay, in milliseconds.
- * @param {string=} opt_description A description to accompany the timeout.
- * @return {!Thenable<void>} A promise that will be resolved when the timeout
- * fires.
- */
- timeout(ms, opt_description) {}
- /**
- * Schedules a task to wait for a condition to hold.
- *
- * If the condition is defined as a function, it may return any value. Promise
- * will be resolved before testing if the condition holds (resolution time
- * counts towards the timeout). Once resolved, values are always evaluated as
- * booleans.
- *
- * If the condition function throws, or returns a rejected promise, the
- * wait task will fail.
- *
- * If the condition is defined as a promise, the scheduler will wait for it to
- * settle. If the timeout expires before the promise settles, the promise
- * returned by this function will be rejected.
- *
- * If this function is invoked with `timeout === 0`, or the timeout is
- * omitted, this scheduler will wait indefinitely for the condition to be
- * satisfied.
- *
- * @param {(!IThenable<T>|function())} condition The condition to poll,
- * or a promise to wait on.
- * @param {number=} opt_timeout How long to wait, in milliseconds, for the
- * condition to hold before timing out. If omitted, the flow will wait
- * indefinitely.
- * @param {string=} opt_message An optional error message to include if the
- * wait times out; defaults to the empty string.
- * @return {!Thenable<T>} A promise that will be fulfilled
- * when the condition has been satisfied. The promise shall be rejected
- * if the wait times out waiting for the condition.
- * @throws {TypeError} If condition is not a function or promise or if timeout
- * is not a number >= 0.
- * @template T
- */
- wait(condition, opt_timeout, opt_message) {}
- }
- let USE_PROMISE_MANAGER;
- function usePromiseManager() {
- if (typeof USE_PROMISE_MANAGER !== 'undefined') {
- return !!USE_PROMISE_MANAGER;
- }
- return process.env['SELENIUM_PROMISE_MANAGER'] === undefined
- || !/^0|false$/i.test(process.env['SELENIUM_PROMISE_MANAGER']);
- }
- /**
- * Creates a new promise with the given `resolver` function. If the promise
- * manager is currently enabled, the returned promise will be a
- * {@linkplain ManagedPromise} instance. Otherwise, it will be a native promise.
- *
- * @param {function(
- * function((T|IThenable<T>|Thenable|null)=),
- * function(*=))} resolver
- * @return {!Thenable<T>}
- * @template T
- */
- function createPromise(resolver) {
- let ctor = usePromiseManager() ? ManagedPromise : NativePromise;
- return new ctor(resolver);
- }
- /**
- * @param {!Scheduler} scheduler The scheduler to use.
- * @param {(!IThenable<T>|function())} condition The condition to poll,
- * or a promise to wait on.
- * @param {number=} opt_timeout How long to wait, in milliseconds, for the
- * condition to hold before timing out. If omitted, the flow will wait
- * indefinitely.
- * @param {string=} opt_message An optional error message to include if the
- * wait times out; defaults to the empty string.
- * @return {!Thenable<T>} A promise that will be fulfilled
- * when the condition has been satisfied. The promise shall be rejected
- * if the wait times out waiting for the condition.
- * @throws {TypeError} If condition is not a function or promise or if timeout
- * is not a number >= 0.
- * @template T
- */
- function scheduleWait(scheduler, condition, opt_timeout, opt_message) {
- let timeout = opt_timeout || 0;
- if (typeof timeout !== 'number' || timeout < 0) {
- throw TypeError('timeout must be a number >= 0: ' + timeout);
- }
- if (isPromise(condition)) {
- return scheduler.execute(function() {
- if (!timeout) {
- return condition;
- }
- return scheduler.promise(function(fulfill, reject) {
- let start = Date.now();
- let timer = setTimeout(function() {
- timer = null;
- reject(
- new error.TimeoutError(
- (opt_message ? opt_message + '\n' : '')
- + 'Timed out waiting for promise to resolve after '
- + (Date.now() - start) + 'ms'));
- }, timeout);
- /** @type {Thenable} */(condition).then(
- function(value) {
- timer && clearTimeout(timer);
- fulfill(value);
- },
- function(error) {
- timer && clearTimeout(timer);
- reject(error);
- });
- });
- }, opt_message || '<anonymous wait: promise resolution>');
- }
- if (typeof condition !== 'function') {
- throw TypeError('Invalid condition; must be a function or promise: ' +
- typeof condition);
- }
- if (isGenerator(condition)) {
- let original = condition;
- condition = () => consume(original);
- }
- return scheduler.execute(function() {
- var startTime = Date.now();
- return scheduler.promise(function(fulfill, reject) {
- pollCondition();
- function pollCondition() {
- var conditionFn = /** @type {function()} */(condition);
- scheduler.execute(conditionFn).then(function(value) {
- var elapsed = Date.now() - startTime;
- if (!!value) {
- fulfill(value);
- } else if (timeout && elapsed >= timeout) {
- reject(
- new error.TimeoutError(
- (opt_message ? opt_message + '\n' : '')
- + `Wait timed out after ${elapsed}ms`));
- } else {
- // Do not use asyncRun here because we need a non-micro yield
- // here so the UI thread is given a chance when running in a
- // browser.
- setTimeout(pollCondition, 0);
- }
- }, reject);
- }
- });
- }, opt_message || '<anonymous wait>');
- }
- /**
- * A scheduler that executes all tasks immediately, with no coordination. This
- * class is an event emitter for API compatibility with the {@link ControlFlow},
- * however, it emits no events.
- *
- * @implements {Scheduler}
- */
- class SimpleScheduler extends events.EventEmitter {
- /** @override */
- execute(fn) {
- return this.promise((resolve, reject) => {
- try {
- if (isGenerator(fn)) {
- consume(fn).then(resolve, reject);
- } else {
- resolve(fn.call(undefined));
- }
- } catch (ex) {
- reject(ex);
- }
- });
- }
- /** @override */
- promise(resolver) {
- return new NativePromise(resolver);
- }
- /** @override */
- timeout(ms) {
- return this.promise(resolve => setTimeout(_ => resolve(), ms));
- }
- /** @override */
- wait(condition, opt_timeout, opt_message) {
- return scheduleWait(this, condition, opt_timeout, opt_message);
- }
- }
- const SIMPLE_SCHEDULER = new SimpleScheduler;
- /**
- * Handles the execution of scheduled tasks, each of which may be an
- * asynchronous operation. The control flow will ensure tasks are executed in
- * the order scheduled, starting each task only once those before it have
- * completed.
- *
- * Each task scheduled within this flow may return a {@link ManagedPromise} to
- * indicate it is an asynchronous operation. The ControlFlow will wait for such
- * promises to be resolved before marking the task as completed.
- *
- * Tasks and each callback registered on a {@link ManagedPromise} will be run
- * in their own ControlFlow frame. Any tasks scheduled within a frame will take
- * priority over previously scheduled tasks. Furthermore, if any of the tasks in
- * the frame fail, the remainder of the tasks in that frame will be discarded
- * and the failure will be propagated to the user through the callback/task's
- * promised result.
- *
- * Each time a ControlFlow empties its task queue, it will fire an
- * {@link ControlFlow.EventType.IDLE IDLE} event. Conversely, whenever
- * the flow terminates due to an unhandled error, it will remove all
- * remaining tasks in its queue and fire an
- * {@link ControlFlow.EventType.UNCAUGHT_EXCEPTION UNCAUGHT_EXCEPTION} event.
- * If there are no listeners registered with the flow, the error will be
- * rethrown to the global error handler.
- *
- * Refer to the {@link ./promise} module documentation for a detailed
- * explanation of how the ControlFlow coordinates task execution.
- *
- * @implements {Scheduler}
- * @final
- */
- class ControlFlow extends events.EventEmitter {
- constructor() {
- if (!usePromiseManager()) {
- throw TypeError(
- 'Cannot instantiate control flow when the promise manager has'
- + ' been disabled');
- }
- super();
- /** @private {boolean} */
- this.propagateUnhandledRejections_ = true;
- /** @private {TaskQueue} */
- this.activeQueue_ = null;
- /** @private {Set<TaskQueue>} */
- this.taskQueues_ = null;
- /**
- * Microtask that controls shutting down the control flow. Upon shut down,
- * the flow will emit an
- * {@link ControlFlow.EventType.IDLE} event. Idle events
- * always follow a brief timeout in order to catch latent errors from the
- * last completed task. If this task had a callback registered, but no
- * errback, and the task fails, the unhandled failure would not be reported
- * by the promise system until the next turn of the event loop:
- *
- * // Schedule 1 task that fails.
- * var result = promise.controlFlow().execute(
- * () => promise.rejected('failed'), 'example');
- * // Set a callback on the result. This delays reporting the unhandled
- * // failure for 1 turn of the event loop.
- * result.then(function() {});
- *
- * @private {MicroTask}
- */
- this.shutdownTask_ = null;
- /**
- * ID for a long running interval used to keep a Node.js process running
- * while a control flow's event loop is still working. This is a cheap hack
- * required since JS events are only scheduled to run when there is
- * _actually_ something to run. When a control flow is waiting on a task,
- * there will be nothing in the JS event loop and the process would
- * terminate without this.
- * @private
- */
- this.hold_ = null;
- }
- /**
- * Returns a string representation of this control flow, which is its current
- * {@linkplain #getSchedule() schedule}, sans task stack traces.
- * @return {string} The string representation of this control flow.
- * @override
- */
- toString() {
- return this.getSchedule();
- }
- /**
- * Sets whether any unhandled rejections should propagate up through the
- * control flow stack and cause rejections within parent tasks. If error
- * propagation is disabled, tasks will not be aborted when an unhandled
- * promise rejection is detected, but the rejection _will_ trigger an
- * {@link ControlFlow.EventType.UNCAUGHT_EXCEPTION} event.
- *
- * The default behavior is to propagate all unhandled rejections. _The use
- * of this option is highly discouraged._
- *
- * @param {boolean} propagate whether to propagate errors.
- */
- setPropagateUnhandledRejections(propagate) {
- this.propagateUnhandledRejections_ = propagate;
- }
- /**
- * @return {boolean} Whether this flow is currently idle.
- */
- isIdle() {
- return !this.shutdownTask_ && (!this.taskQueues_ || !this.taskQueues_.size);
- }
- /**
- * Resets this instance, clearing its queue and removing all event listeners.
- */
- reset() {
- this.cancelQueues_(new FlowResetError);
- this.emit(ControlFlow.EventType.RESET);
- this.removeAllListeners();
- this.cancelShutdown_();
- }
- /**
- * Generates an annotated string describing the internal state of this control
- * flow, including the currently executing as well as pending tasks. If
- * {@code opt_includeStackTraces === true}, the string will include the
- * stack trace from when each task was scheduled.
- * @param {string=} opt_includeStackTraces Whether to include the stack traces
- * from when each task was scheduled. Defaults to false.
- * @return {string} String representation of this flow's internal state.
- */
- getSchedule(opt_includeStackTraces) {
- var ret = 'ControlFlow::' + getUid(this);
- var activeQueue = this.activeQueue_;
- if (!this.taskQueues_ || !this.taskQueues_.size) {
- return ret;
- }
- var childIndent = '| ';
- for (var q of this.taskQueues_) {
- ret += '\n' + printQ(q, childIndent);
- }
- return ret;
- function printQ(q, indent) {
- var ret = q.toString();
- if (q === activeQueue) {
- ret = '(active) ' + ret;
- }
- var prefix = indent + childIndent;
- if (q.pending_) {
- if (q.pending_.q.state_ !== TaskQueueState.FINISHED) {
- ret += '\n' + prefix + '(pending) ' + q.pending_.task;
- ret += '\n' + printQ(q.pending_.q, prefix + childIndent);
- } else {
- ret += '\n' + prefix + '(blocked) ' + q.pending_.task;
- }
- }
- if (q.interrupts_) {
- q.interrupts_.forEach((task) => {
- ret += '\n' + prefix + task;
- });
- }
- if (q.tasks_) {
- q.tasks_.forEach((task) => ret += printTask(task, '\n' + prefix));
- }
- return indent + ret;
- }
- function printTask(task, prefix) {
- var ret = prefix + task;
- if (opt_includeStackTraces && task.promise.stack_) {
- ret += prefix + childIndent
- + (task.promise.stack_.stack || task.promise.stack_)
- .replace(/\n/g, prefix);
- }
- return ret;
- }
- }
- /**
- * Returns the currently active task queue for this flow. If there is no
- * active queue, one will be created.
- * @return {!TaskQueue} the currently active task queue for this flow.
- * @private
- */
- getActiveQueue_() {
- if (this.activeQueue_) {
- return this.activeQueue_;
- }
- this.activeQueue_ = new TaskQueue(this);
- if (!this.taskQueues_) {
- this.taskQueues_ = new Set();
- }
- this.taskQueues_.add(this.activeQueue_);
- this.activeQueue_
- .once('end', this.onQueueEnd_, this)
- .once('error', this.onQueueError_, this);
- asyncRun(() => this.activeQueue_ = null);
- this.activeQueue_.start();
- return this.activeQueue_;
- }
- /** @override */
- execute(fn, opt_description) {
- if (isGenerator(fn)) {
- let original = fn;
- fn = () => consume(original);
- }
- if (!this.hold_) {
- let holdIntervalMs = 2147483647; // 2^31-1; max timer length for Node.js
- this.hold_ = setInterval(function() {}, holdIntervalMs);
- }
- let task = new Task(
- this, fn, opt_description || '<anonymous>',
- {name: 'Task', top: ControlFlow.prototype.execute},
- true);
- let q = this.getActiveQueue_();
- for (let i = q.tasks_.length; i > 0; i--) {
- let previousTask = q.tasks_[i - 1];
- if (previousTask.userTask_) {
- FLOW_LOG.warning(() => {
- return `Detected scheduling of an unchained task.
- When the promise manager is disabled, unchained tasks will not wait for
- previously scheduled tasks to finish before starting to execute.
- New task: ${task.promise.stack_.stack}
- Previous task: ${previousTask.promise.stack_.stack}`.split(/\n/).join('\n ');
- });
- break;
- }
- }
- q.enqueue(task);
- this.emit(ControlFlow.EventType.SCHEDULE_TASK, task.description);
- return task.promise;
- }
- /** @override */
- promise(resolver) {
- return new ManagedPromise(resolver, this, SKIP_LOG);
- }
- /** @override */
- timeout(ms, opt_description) {
- return this.execute(() => {
- return this.promise(resolve => setTimeout(() => resolve(), ms));
- }, opt_description);
- }
- /** @override */
- wait(condition, opt_timeout, opt_message) {
- return scheduleWait(this, condition, opt_timeout, opt_message);
- }
- /**
- * Executes a function in the next available turn of the JavaScript event
- * loop. This ensures the function runs with its own task queue and any
- * scheduled tasks will run in "parallel" to those scheduled in the current
- * function.
- *
- * flow.execute(() => console.log('a'));
- * flow.execute(() => console.log('b'));
- * flow.execute(() => console.log('c'));
- * flow.async(() => {
- * flow.execute(() => console.log('d'));
- * flow.execute(() => console.log('e'));
- * });
- * flow.async(() => {
- * flow.execute(() => console.log('f'));
- * flow.execute(() => console.log('g'));
- * });
- * flow.once('idle', () => console.log('fin'));
- * // a
- * // d
- * // f
- * // b
- * // e
- * // g
- * // c
- * // fin
- *
- * If the function itself throws, the error will be treated the same as an
- * unhandled rejection within the control flow.
- *
- * __NOTE__: This function is considered _unstable_.
- *
- * @param {!Function} fn The function to execute.
- * @param {Object=} opt_self The object in whose context to run the function.
- * @param {...*} var_args Any arguments to pass to the function.
- */
- async(fn, opt_self, var_args) {
- asyncRun(() => {
- // Clear any lingering queues, forces getActiveQueue_ to create a new one.
- this.activeQueue_ = null;
- var q = this.getActiveQueue_();
- try {
- q.execute_(fn.bind(opt_self, var_args));
- } catch (ex) {
- var cancellationError = CancellationError.wrap(ex,
- 'Function passed to ControlFlow.async() threw');
- cancellationError.silent_ = true;
- q.abort_(cancellationError);
- } finally {
- this.activeQueue_ = null;
- }
- });
- }
- /**
- * Event handler for when a task queue is exhausted. This starts the shutdown
- * sequence for this instance if there are no remaining task queues: after
- * one turn of the event loop, this object will emit the
- * {@link ControlFlow.EventType.IDLE IDLE} event to signal
- * listeners that it has completed. During this wait, if another task is
- * scheduled, the shutdown will be aborted.
- *
- * @param {!TaskQueue} q the completed task queue.
- * @private
- */
- onQueueEnd_(q) {
- if (!this.taskQueues_) {
- return;
- }
- this.taskQueues_.delete(q);
- vlog(1, () => q + ' has finished');
- vlog(1, () => this.taskQueues_.size + ' queues remain\n' + this, this);
- if (!this.taskQueues_.size) {
- if (this.shutdownTask_) {
- throw Error('Already have a shutdown task??');
- }
- vlog(1, () => 'Scheduling shutdown\n' + this);
- this.shutdownTask_ = new MicroTask(() => this.shutdown_());
- }
- }
- /**
- * Event handler for when a task queue terminates with an error. This triggers
- * the cancellation of all other task queues and a
- * {@link ControlFlow.EventType.UNCAUGHT_EXCEPTION} event.
- * If there are no error event listeners registered with this instance, the
- * error will be rethrown to the global error handler.
- *
- * @param {*} error the error that caused the task queue to terminate.
- * @param {!TaskQueue} q the task queue.
- * @private
- */
- onQueueError_(error, q) {
- if (this.taskQueues_) {
- this.taskQueues_.delete(q);
- }
- this.cancelQueues_(CancellationError.wrap(
- error, 'There was an uncaught error in the control flow'));
- this.cancelShutdown_();
- this.cancelHold_();
- setTimeout(() => {
- let listeners = this.listeners(ControlFlow.EventType.UNCAUGHT_EXCEPTION);
- if (!listeners.size) {
- throw error;
- } else {
- this.reportUncaughtException_(error);
- }
- }, 0);
- }
- /**
- * Cancels all remaining task queues.
- * @param {!CancellationError} reason The cancellation reason.
- * @private
- */
- cancelQueues_(reason) {
- reason.silent_ = true;
- if (this.taskQueues_) {
- for (var q of this.taskQueues_) {
- q.removeAllListeners();
- q.abort_(reason);
- }
- this.taskQueues_.clear();
- this.taskQueues_ = null;
- }
- }
- /**
- * Reports an uncaught exception using a
- * {@link ControlFlow.EventType.UNCAUGHT_EXCEPTION} event.
- *
- * @param {*} e the error to report.
- * @private
- */
- reportUncaughtException_(e) {
- this.emit(ControlFlow.EventType.UNCAUGHT_EXCEPTION, e);
- }
- /** @private */
- cancelHold_() {
- if (this.hold_) {
- clearInterval(this.hold_);
- this.hold_ = null;
- }
- }
- /** @private */
- shutdown_() {
- vlog(1, () => 'Going idle: ' + this);
- this.cancelHold_();
- this.shutdownTask_ = null;
- this.emit(ControlFlow.EventType.IDLE);
- }
- /**
- * Cancels the shutdown sequence if it is currently scheduled.
- * @private
- */
- cancelShutdown_() {
- if (this.shutdownTask_) {
- this.shutdownTask_.cancel();
- this.shutdownTask_ = null;
- }
- }
- }
- /**
- * Events that may be emitted by an {@link ControlFlow}.
- * @enum {string}
- */
- ControlFlow.EventType = {
- /** Emitted when all tasks have been successfully executed. */
- IDLE: 'idle',
- /** Emitted when a ControlFlow has been reset. */
- RESET: 'reset',
- /** Emitted whenever a new task has been scheduled. */
- SCHEDULE_TASK: 'scheduleTask',
- /**
- * Emitted whenever a control flow aborts due to an unhandled promise
- * rejection. This event will be emitted along with the offending rejection
- * reason. Upon emitting this event, the control flow will empty its task
- * queue and revert to its initial state.
- */
- UNCAUGHT_EXCEPTION: 'uncaughtException'
- };
- /**
- * Wraps a function to execute as a cancellable micro task.
- * @final
- */
- class MicroTask {
- /**
- * @param {function()} fn The function to run as a micro task.
- */
- constructor(fn) {
- /** @private {boolean} */
- this.cancelled_ = false;
- asyncRun(() => {
- if (!this.cancelled_) {
- fn();
- }
- });
- }
- /**
- * Runs the given function after a microtask yield.
- * @param {function()} fn The function to run.
- */
- static run(fn) {
- NativePromise.resolve().then(function() {
- try {
- fn();
- } catch (ignored) {
- // Do nothing.
- }
- });
- }
- /**
- * Cancels the execution of this task. Note: this will not prevent the task
- * timer from firing, just the invocation of the wrapped function.
- */
- cancel() {
- this.cancelled_ = true;
- }
- }
- /**
- * A task to be executed by a {@link ControlFlow}.
- *
- * @template T
- * @final
- */
- class Task extends Deferred {
- /**
- * @param {!ControlFlow} flow The flow this instances belongs
- * to.
- * @param {function(): (T|!ManagedPromise<T>)} fn The function to
- * call when the task executes. If it returns a
- * {@link ManagedPromise}, the flow will wait for it to be
- * resolved before starting the next task.
- * @param {string} description A description of the task for debugging.
- * @param {{name: string, top: !Function}=} opt_stackOptions Options to use
- * when capturing the stacktrace for when this task was created.
- * @param {boolean=} opt_isUserTask Whether this task was explicitly scheduled
- * by the use of the promise manager.
- */
- constructor(flow, fn, description, opt_stackOptions, opt_isUserTask) {
- super(flow, SKIP_LOG);
- getUid(this);
- /** @type {function(): (T|!ManagedPromise<T>)} */
- this.execute = fn;
- /** @type {string} */
- this.description = description;
- /** @type {TaskQueue} */
- this.queue = null;
- /** @private @const {boolean} */
- this.userTask_ = !!opt_isUserTask;
- /**
- * Whether this task is considered block. A blocked task may be registered
- * in a task queue, but will be dropped if it is still blocked when it
- * reaches the front of the queue. A dropped task may always be rescheduled.
- *
- * Blocked tasks are used when a callback is attached to an unsettled
- * promise to reserve a spot in line (in a manner of speaking). If the
- * promise is not settled before the callback reaches the front of the
- * of the queue, it will be dropped. Once the promise is settled, the
- * dropped task will be rescheduled as an interrupt on the currently task
- * queue.
- *
- * @type {boolean}
- */
- this.blocked = false;
- if (opt_stackOptions) {
- this.promise.stack_ = captureStackTrace(
- opt_stackOptions.name, this.description, opt_stackOptions.top);
- }
- }
- /** @override */
- toString() {
- return 'Task::' + getUid(this) + '<' + this.description + '>';
- }
- }
- /** @enum {string} */
- const TaskQueueState = {
- NEW: 'new',
- STARTED: 'started',
- FINISHED: 'finished'
- };
- /**
- * @final
- */
- class TaskQueue extends events.EventEmitter {
- /** @param {!ControlFlow} flow . */
- constructor(flow) {
- super();
- /** @private {string} */
- this.name_ = 'TaskQueue::' + getUid(this);
- /** @private {!ControlFlow} */
- this.flow_ = flow;
- /** @private {!Array<!Task>} */
- this.tasks_ = [];
- /** @private {Array<!Task>} */
- this.interrupts_ = null;
- /** @private {({task: !Task, q: !TaskQueue}|null)} */
- this.pending_ = null;
- /** @private {TaskQueue} */
- this.subQ_ = null;
- /** @private {TaskQueueState} */
- this.state_ = TaskQueueState.NEW;
- /** @private {!Set<!ManagedPromise>} */
- this.unhandledRejections_ = new Set();
- }
- /** @override */
- toString() {
- return 'TaskQueue::' + getUid(this);
- }
- /**
- * @param {!ManagedPromise} promise .
- */
- addUnhandledRejection(promise) {
- // TODO: node 4.0.0+
- vlog(2, () => this + ' registering unhandled rejection: ' + promise, this);
- this.unhandledRejections_.add(promise);
- }
- /**
- * @param {!ManagedPromise} promise .
- */
- clearUnhandledRejection(promise) {
- var deleted = this.unhandledRejections_.delete(promise);
- if (deleted) {
- // TODO: node 4.0.0+
- vlog(2, () => this + ' clearing unhandled rejection: ' + promise, this);
- }
- }
- /**
- * Enqueues a new task for execution.
- * @param {!Task} task The task to enqueue.
- * @throws {Error} If this instance has already started execution.
- */
- enqueue(task) {
- if (this.state_ !== TaskQueueState.NEW) {
- throw Error('TaskQueue has started: ' + this);
- }
- if (task.queue) {
- throw Error('Task is already scheduled in another queue');
- }
- this.tasks_.push(task);
- task.queue = this;
- ON_CANCEL_HANDLER.set(
- task.promise,
- (e) => this.onTaskCancelled_(task, e));
- vlog(1, () => this + '.enqueue(' + task + ')', this);
- vlog(2, () => this.flow_.toString(), this);
- }
- /**
- * Schedules the callbacks registered on the given promise in this queue.
- *
- * @param {!ManagedPromise} promise the promise whose callbacks should be
- * registered as interrupts in this task queue.
- * @throws {Error} if this queue has already finished.
- */
- scheduleCallbacks(promise) {
- if (this.state_ === TaskQueueState.FINISHED) {
- throw new Error('cannot interrupt a finished q(' + this + ')');
- }
- if (this.pending_ && this.pending_.task.promise === promise) {
- this.pending_.task.promise.queue_ = null;
- this.pending_ = null;
- asyncRun(() => this.executeNext_());
- }
- if (!promise.callbacks_) {
- return;
- }
- promise.callbacks_.forEach(function(cb) {
- cb.blocked = false;
- if (cb.queue) {
- return;
- }
- ON_CANCEL_HANDLER.set(
- cb.promise,
- (e) => this.onTaskCancelled_(cb, e));
- if (cb.queue === this && this.tasks_.indexOf(cb) !== -1) {
- return;
- }
- if (cb.queue) {
- cb.queue.dropTask_(cb);
- }
- cb.queue = this;
- if (!this.interrupts_) {
- this.interrupts_ = [];
- }
- this.interrupts_.push(cb);
- }, this);
- promise.callbacks_ = null;
- vlog(2, () => this + ' interrupted\n' + this.flow_, this);
- }
- /**
- * Starts executing tasks in this queue. Once called, no further tasks may
- * be {@linkplain #enqueue() enqueued} with this instance.
- *
- * @throws {Error} if this queue has already been started.
- */
- start() {
- if (this.state_ !== TaskQueueState.NEW) {
- throw new Error('TaskQueue has already started');
- }
- // Always asynchronously execute next, even if there doesn't look like
- // there is anything in the queue. This will catch pending unhandled
- // rejections that were registered before start was called.
- asyncRun(() => this.executeNext_());
- }
- /**
- * Aborts this task queue. If there are any scheduled tasks, they are silently
- * cancelled and discarded (their callbacks will never fire). If this queue
- * has a _pending_ task, the abortion error is used to cancel that task.
- * Otherwise, this queue will emit an error event.
- *
- * @param {*} error The abortion reason.
- * @private
- */
- abort_(error) {
- var cancellation;
- if (error instanceof FlowResetError) {
- cancellation = error;
- } else {
- cancellation = new DiscardedTaskError(error);
- }
- if (this.interrupts_ && this.interrupts_.length) {
- this.interrupts_.forEach((t) => t.reject(cancellation));
- this.interrupts_ = [];
- }
- if (this.tasks_ && this.tasks_.length) {
- this.tasks_.forEach((t) => t.reject(cancellation));
- this.tasks_ = [];
- }
- // Now that all of the remaining tasks have been silently cancelled (e.g. no
- // existing callbacks on those tasks will fire), clear the silence bit on
- // the cancellation error. This ensures additional callbacks registered in
- // the future will actually execute.
- cancellation.silent_ = false;
- if (this.pending_) {
- vlog(2, () => this + '.abort(); cancelling pending task', this);
- this.pending_.task.promise.cancel(
- /** @type {!CancellationError} */(error));
- } else {
- vlog(2, () => this + '.abort(); emitting error event', this);
- this.emit('error', error, this);
- }
- }
- /** @private */
- executeNext_() {
- if (this.state_ === TaskQueueState.FINISHED) {
- return;
- }
- this.state_ = TaskQueueState.STARTED;
- if (this.pending_ !== null || this.processUnhandledRejections_()) {
- return;
- }
- var task;
- do {
- task = this.getNextTask_();
- } while (task && !isPending(task.promise));
- if (!task) {
- this.state_ = TaskQueueState.FINISHED;
- this.tasks_ = [];
- this.interrupts_ = null;
- vlog(2, () => this + '.emit(end)', this);
- this.emit('end', this);
- return;
- }
- let result = undefined;
- this.subQ_ = new TaskQueue(this.flow_);
- this.subQ_.once('end', () => { // On task completion.
- this.subQ_ = null;
- this.pending_ && this.pending_.task.resolve(result);
- });
- this.subQ_.once('error', e => { // On task failure.
- this.subQ_ = null;
- if (Thenable.isImplementation(result)) {
- result.cancel(CancellationError.wrap(e));
- }
- this.pending_ && this.pending_.task.reject(e);
- });
- vlog(2, () => `${this} created ${this.subQ_} for ${task}`);
- try {
- this.pending_ = {task: task, q: this.subQ_};
- task.promise.queue_ = this;
- result = this.subQ_.execute_(task.execute);
- this.subQ_.start();
- } catch (ex) {
- this.subQ_.abort_(ex);
- }
- }
- /**
- * @param {!Function} fn .
- * @return {T} .
- * @template T
- * @private
- */
- execute_(fn) {
- try {
- activeFlows.push(this.flow_);
- this.flow_.activeQueue_ = this;
- return fn();
- } finally {
- this.flow_.activeQueue_ = null;
- activeFlows.pop();
- }
- }
- /**
- * Process any unhandled rejections registered with this task queue. If there
- * is a rejection, this queue will be aborted with the rejection error. If
- * there are multiple rejections registered, this queue will be aborted with
- * a {@link MultipleUnhandledRejectionError}.
- * @return {boolean} whether there was an unhandled rejection.
- * @private
- */
- processUnhandledRejections_() {
- if (!this.unhandledRejections_.size) {
- return false;
- }
- var errors = new Set();
- for (var rejection of this.unhandledRejections_) {
- errors.add(rejection.value_);
- }
- this.unhandledRejections_.clear();
- var errorToReport = errors.size === 1
- ? errors.values().next().value
- : new MultipleUnhandledRejectionError(errors);
- vlog(1, () => this + ' aborting due to unhandled rejections', this);
- if (this.flow_.propagateUnhandledRejections_) {
- this.abort_(errorToReport);
- return true;
- } else {
- vlog(1, 'error propagation disabled; reporting to control flow');
- this.flow_.reportUncaughtException_(errorToReport);
- return false;
- }
- }
- /**
- * @param {!Task} task The task to drop.
- * @private
- */
- dropTask_(task) {
- var index;
- if (this.interrupts_) {
- index = this.interrupts_.indexOf(task);
- if (index != -1) {
- task.queue = null;
- this.interrupts_.splice(index, 1);
- return;
- }
- }
- index = this.tasks_.indexOf(task);
- if (index != -1) {
- task.queue = null;
- this.tasks_.splice(index, 1);
- }
- }
- /**
- * @param {!Task} task The task that was cancelled.
- * @param {!CancellationError} reason The cancellation reason.
- * @private
- */
- onTaskCancelled_(task, reason) {
- if (this.pending_ && this.pending_.task === task) {
- this.pending_.q.abort_(reason);
- } else {
- this.dropTask_(task);
- }
- }
- /**
- * @return {(Task|undefined)} the next task scheduled within this queue,
- * if any.
- * @private
- */
- getNextTask_() {
- var task = undefined;
- while (true) {
- if (this.interrupts_) {
- task = this.interrupts_.shift();
- }
- if (!task && this.tasks_) {
- task = this.tasks_.shift();
- }
- if (task && task.blocked) {
- vlog(2, () => this + ' skipping blocked task ' + task, this);
- task.queue = null;
- task = null;
- // TODO: recurse when tail-call optimization is available in node.
- } else {
- break;
- }
- }
- return task;
- }
- }
- /**
- * The default flow to use if no others are active.
- * @type {ControlFlow}
- */
- var defaultFlow;
- /**
- * A stack of active control flows, with the top of the stack used to schedule
- * commands. When there are multiple flows on the stack, the flow at index N
- * represents a callback triggered within a task owned by the flow at index
- * N-1.
- * @type {!Array<!ControlFlow>}
- */
- var activeFlows = [];
- /**
- * Changes the default flow to use when no others are active.
- * @param {!ControlFlow} flow The new default flow.
- * @throws {Error} If the default flow is not currently active.
- */
- function setDefaultFlow(flow) {
- if (!usePromiseManager()) {
- throw Error(
- 'You may not change set the control flow when the promise'
- +' manager is disabled');
- }
- if (activeFlows.length) {
- throw Error('You may only change the default flow while it is active');
- }
- defaultFlow = flow;
- }
- /**
- * @return {!ControlFlow} The currently active control flow.
- * @suppress {checkTypes}
- */
- function controlFlow() {
- if (!usePromiseManager()) {
- return SIMPLE_SCHEDULER;
- }
- if (activeFlows.length) {
- return activeFlows[activeFlows.length - 1];
- }
- if (!defaultFlow) {
- defaultFlow = new ControlFlow;
- }
- return defaultFlow;
- }
- /**
- * Creates a new control flow. The provided callback will be invoked as the
- * first task within the new flow, with the flow as its sole argument. Returns
- * a promise that resolves to the callback result.
- * @param {function(!ControlFlow)} callback The entry point
- * to the newly created flow.
- * @return {!Thenable} A promise that resolves to the callback result.
- */
- function createFlow(callback) {
- var flow = new ControlFlow;
- return flow.execute(function() {
- return callback(flow);
- });
- }
- /**
- * Tests is a function is a generator.
- * @param {!Function} fn The function to test.
- * @return {boolean} Whether the function is a generator.
- */
- function isGenerator(fn) {
- return fn.constructor.name === 'GeneratorFunction';
- }
- /**
- * Consumes a {@code GeneratorFunction}. Each time the generator yields a
- * promise, this function will wait for it to be fulfilled before feeding the
- * fulfilled value back into {@code next}. Likewise, if a yielded promise is
- * rejected, the rejection error will be passed to {@code throw}.
- *
- * __Example 1:__ the Fibonacci Sequence.
- *
- * promise.consume(function* fibonacci() {
- * var n1 = 1, n2 = 1;
- * for (var i = 0; i < 4; ++i) {
- * var tmp = yield n1 + n2;
- * n1 = n2;
- * n2 = tmp;
- * }
- * return n1 + n2;
- * }).then(function(result) {
- * console.log(result); // 13
- * });
- *
- * __Example 2:__ a generator that throws.
- *
- * promise.consume(function* () {
- * yield promise.delayed(250).then(function() {
- * throw Error('boom');
- * });
- * }).catch(function(e) {
- * console.log(e.toString()); // Error: boom
- * });
- *
- * @param {!Function} generatorFn The generator function to execute.
- * @param {Object=} opt_self The object to use as "this" when invoking the
- * initial generator.
- * @param {...*} var_args Any arguments to pass to the initial generator.
- * @return {!Thenable<?>} A promise that will resolve to the
- * generator's final result.
- * @throws {TypeError} If the given function is not a generator.
- */
- function consume(generatorFn, opt_self, ...var_args) {
- if (!isGenerator(generatorFn)) {
- throw new TypeError('Input is not a GeneratorFunction: ' +
- generatorFn.constructor.name);
- }
- let ret;
- return ret = createPromise((resolve, reject) => {
- let generator = generatorFn.apply(opt_self, var_args);
- callNext();
- /** @param {*=} opt_value . */
- function callNext(opt_value) {
- pump(generator.next, opt_value);
- }
- /** @param {*=} opt_error . */
- function callThrow(opt_error) {
- pump(generator.throw, opt_error);
- }
- function pump(fn, opt_arg) {
- if (ret instanceof ManagedPromise && !isPending(ret)) {
- return; // Deferred was cancelled; silently abort.
- }
- try {
- var result = fn.call(generator, opt_arg);
- } catch (ex) {
- reject(ex);
- return;
- }
- if (result.done) {
- resolve(result.value);
- return;
- }
- asap(result.value, callNext, callThrow);
- }
- });
- }
- // PUBLIC API
- module.exports = {
- CancellableThenable: CancellableThenable,
- CancellationError: CancellationError,
- ControlFlow: ControlFlow,
- Deferred: Deferred,
- MultipleUnhandledRejectionError: MultipleUnhandledRejectionError,
- Thenable: Thenable,
- Promise: ManagedPromise,
- Resolver: Resolver,
- Scheduler: Scheduler,
- all: all,
- asap: asap,
- captureStackTrace: captureStackTrace,
- checkedNodeCall: checkedNodeCall,
- consume: consume,
- controlFlow: controlFlow,
- createFlow: createFlow,
- createPromise: createPromise,
- defer: defer,
- delayed: delayed,
- filter: filter,
- finally: thenFinally,
- fulfilled: fulfilled,
- fullyResolved: fullyResolved,
- isGenerator: isGenerator,
- isPromise: isPromise,
- map: map,
- rejected: rejected,
- setDefaultFlow: setDefaultFlow,
- when: when,
- /**
- * Indicates whether the promise manager is currently enabled. When disabled,
- * attempting to use the {@link ControlFlow} or {@link ManagedPromise Promise}
- * classes will generate an error.
- *
- * The promise manager is currently enabled by default, but may be disabled
- * by setting the environment variable `SELENIUM_PROMISE_MANAGER=0` or by
- * setting this property to false. Setting this property will always take
- * precedence over the use of the environment variable.
- *
- * @return {boolean} Whether the promise manager is enabled.
- * @see <https://github.com/SeleniumHQ/selenium/issues/2969>
- */
- get USE_PROMISE_MANAGER() { return usePromiseManager(); },
- set USE_PROMISE_MANAGER(/** boolean */value) { USE_PROMISE_MANAGER = value; },
- get LONG_STACK_TRACES() { return LONG_STACK_TRACES; },
- set LONG_STACK_TRACES(v) { LONG_STACK_TRACES = v; },
- };
|