/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * vim: set ts=8 sts=4 et sw=4 tw=99: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef js_Promise_h #define js_Promise_h #include "mozilla/Attributes.h" #include "jstypes.h" #include "js/RootingAPI.h" #include "js/TypeDecls.h" #include "js/UniquePtr.h" namespace JS { class JS_PUBLIC_API AutoDebuggerJobQueueInterruption; /** * Abstract base class for an ECMAScript Job Queue: * https://www.ecma-international.org/ecma-262/9.0/index.html#sec-jobs-and-job-queues * * SpiderMonkey doesn't schedule Promise resolution jobs itself; instead, the * embedding can provide an instance of this class SpiderMonkey can use to do * that scheduling. * * The JavaScript shell includes a simple implementation adequate for running * tests. Browsers need to augment job handling to meet their own additional * requirements, so they can provide their own implementation. */ class JS_PUBLIC_API JobQueue { public: virtual ~JobQueue() = default; /** * Ask the embedding for the incumbent global. * * SpiderMonkey doesn't itself have a notion of incumbent globals as defined * by the HTML spec, so we need the embedding to provide this. See * dom/script/ScriptSettings.h for details. */ virtual JSObject* getIncumbentGlobal(JSContext* cx) = 0; /** * Enqueue a reaction job `job` for `promise`, which was allocated at * `allocationSite`. Provide `incumbentGlobal` as the incumbent global for * the reaction job's execution. * * `promise` can be null if the promise is optimized out. * `promise` is guaranteed not to be optimized out if the promise has * non-default user-interaction flag. */ virtual bool enqueuePromiseJob(JSContext* cx, JS::HandleObject promise, JS::HandleObject job, JS::HandleObject allocationSite, JS::HandleObject incumbentGlobal) = 0; /** * Run all jobs in the queue. Running one job may enqueue others; continue to * run jobs until the queue is empty. * * Calling this method at the wrong time can break the web. The HTML spec * indicates exactly when the job queue should be drained (in HTML jargon, * when it should "perform a microtask checkpoint"), and doing so at other * times can incompatibly change the semantics of programs that use promises * or other microtask-based features. * * This method is called only via AutoDebuggerJobQueueInterruption, used by * the Debugger API implementation to ensure that the debuggee's job queue is * protected from the debugger's own activity. See the comments on * AutoDebuggerJobQueueInterruption. */ virtual void runJobs(JSContext* cx) = 0; /** * Return true if the job queue is empty, false otherwise. */ virtual bool empty() const = 0; protected: friend class AutoDebuggerJobQueueInterruption; /** * A saved job queue, represented however the JobQueue implementation pleases. * Use AutoDebuggerJobQueueInterruption rather than trying to construct one of * these directly; see documentation there. * * Destructing an instance of this class should assert that the current queue * is empty, and then restore the queue the instance captured. */ class SavedJobQueue { public: virtual ~SavedJobQueue() = default; }; /** * Capture this JobQueue's current job queue as a SavedJobQueue and return it, * leaving the JobQueue's job queue empty. Destroying the returned object * should assert that this JobQueue's current job queue is empty, and restore * the original queue. * * On OOM, this should call JS_ReportOutOfMemory on the given JSContext, * and return a null UniquePtr. */ virtual js::UniquePtr saveJobQueue(JSContext*) = 0; }; /** * Tell SpiderMonkey to use `queue` to schedule promise reactions. * * SpiderMonkey does not take ownership of the queue; it is the embedding's * responsibility to clean it up after the runtime is destroyed. */ extern JS_PUBLIC_API void SetJobQueue(JSContext* cx, JobQueue* queue); /** * [SMDOC] Protecting the debuggee's job/microtask queue from debugger activity. * * When the JavaScript debugger interrupts the execution of some debuggee code * (for a breakpoint, for example), the debuggee's execution must be paused * while the developer takes time to look at it. During this interruption, other * tabs should remain active and usable. If the debuggee shares a main thread * with non-debuggee tabs, that means that the thread will have to process * non-debuggee HTML tasks and microtasks as usual, even as the debuggee's are * on hold until the debugger lets it continue execution. (Letting debuggee * microtasks run during the interruption would mean that, from the debuggee's * point of view, their side effects would take place wherever the breakpoint * was set - in general, not a place other code should ever run, and a violation * of the run-to-completion rule.) * * This means that, even though the timing and ordering of microtasks is * carefully specified by the standard - and important to preserve for * compatibility and predictability - debugger use may, correctly, have the * effect of reordering microtasks. During the interruption, microtasks enqueued * by non-debuggee tabs must run immediately alongside their HTML tasks as * usual, whereas any debuggee microtasks that were in the queue when the * interruption began must wait for the debuggee to be continued - and thus run * after microtasks enqueued after they were. * * Fortunately, this reordering is visible only at the global level: when * implemented correctly, it is not detectable by an individual debuggee. Note * that a debuggee should generally be a complete unit of similar-origin related * browsing contexts. Since non-debuggee activity falls outside that unit, it * should never be visible to the debuggee (except via mechanisms that are * already asynchronous, like events), so the debuggee should be unable to * detect non-debuggee microtasks running when they normally would not. As long * as behavior *visible to the debuggee* is unaffected by the interruption, we * have respected the spirit of the rule. * * Of course, even as we accept the general principle that interrupting the * debuggee should have as little detectable effect as possible, we still permit * the developer to do things like evaluate expressions at the console that have * arbitrary effects on the debuggee's state—effects that could never occur * naturally at that point in the program. But since these are explicitly * requested by the developer, who presumably knows what they're doing, we * support this as best we can. If the developer evaluates an expression in the * console that resolves a promise, it seems most natural for the promise's * reaction microtasks to run immediately, within the interruption. This is an * 'unnatural' time for the microtasks to run, but no more unnatural than the * evaluation that triggered them. * * So the overall behavior we need is as follows: * * - When the debugger interrupts a debuggee, the debuggee's microtask queue * must be saved. * * - When debuggee execution resumes, the debuggee's microtask queue must be * restored exactly as it was when the interruption occurred. * * - Non-debuggee task and microtask execution must take place normally during * the interruption. * * Since each HTML task begins with an empty microtask queue, and it should not * be possible for a task to mix debuggee and non-debuggee code, interrupting a * debuggee should always find a microtask queue containing exclusively debuggee * microtasks, if any. So saving and restoring the microtask queue should affect * only the debuggee, not any non-debuggee content. * * AutoDebuggerJobQueueInterruption * -------------------------------- * * AutoDebuggerJobQueueInterruption is an RAII class, meant for use by the * Debugger API implementation, that takes care of saving and restoring the * queue. * * Constructing and initializing an instance of AutoDebuggerJobQueueInterruption * sets aside the given JSContext's job queue, leaving the JSContext's queue * empty. When the AutoDebuggerJobQueueInterruption instance is destroyed, it * asserts that the JSContext's current job queue (holding jobs enqueued while * the AutoDebuggerJobQueueInterruption was alive) is empty, and restores the * saved queue to the JSContext. * * Since the Debugger API's behavior is up to us, we can specify that Debugger * hooks begin execution with an empty job queue, and that we drain the queue * after each hook function has run. This drain will be visible to debugger * hooks, and makes hook calls resemble HTML tasks, with their own automatic * microtask checkpoint. But, the drain will be invisible to the debuggee, as * its queue is preserved across the hook invocation. * * To protect the debuggee's job queue, Debugger takes care to invoke callback * functions only within the scope of an AutoDebuggerJobQueueInterruption * instance. * * Why not let the hook functions themselves take care of this? * ------------------------------------------------------------ * * Certainly, we could leave responsibility for saving and restoring the job * queue to the Debugger hook functions themselves. * * In fact, early versions of this change tried making the devtools server save * and restore the queue explicitly, but because hooks are set and changed in * numerous places, it was hard to be confident that every case had been * covered, and it seemed that future changes could easily introduce new holes. * * Later versions of this change modified the accessor properties on the * Debugger objects' prototypes to automatically protect the job queue when * calling hooks, but the effect was essentially a monkeypatch applied to an API * we defined and control, which doesn't make sense. * * In the end, since promises have become such a pervasive part of JavaScript * programming, almost any imaginable use of Debugger would need to provide some * kind of protection for the debuggee's job queue, so it makes sense to simply * handle it once, carefully, in the implementation of Debugger itself. */ class MOZ_RAII JS_PUBLIC_API AutoDebuggerJobQueueInterruption { public: explicit AutoDebuggerJobQueueInterruption(); ~AutoDebuggerJobQueueInterruption(); bool init(JSContext* cx); bool initialized() const { return !!saved; } /** * Drain the job queue. (In HTML terminology, perform a microtask checkpoint.) * * To make Debugger hook calls more like HTML tasks or ECMAScript jobs, * Debugger promises that each hook begins execution with a clean microtask * queue, and that a microtask checkpoint (queue drain) takes place after each * hook returns, successfully or otherwise. * * To ensure these debugger-introduced microtask checkpoints serve only the * hook's microtasks, and never affect the debuggee's, the Debugger API * implementation uses only this method to perform the checkpoints, thereby * statically ensuring that an AutoDebuggerJobQueueInterruption is in scope to * protect the debuggee. * * SavedJobQueue implementations are required to assert that the queue is * empty before restoring the debuggee's queue. If the Debugger API ever fails * to perform a microtask checkpoint after calling a hook, that assertion will * fail, catching the mistake. */ void runJobs(); private: JSContext* cx; js::UniquePtr saved; }; enum class PromiseRejectionHandlingState { Unhandled, Handled }; typedef void (*PromiseRejectionTrackerCallback)( JSContext* cx, bool mutedErrors, JS::HandleObject promise, JS::PromiseRejectionHandlingState state, void* data); /** * Sets the callback that's invoked whenever a Promise is rejected without * a rejection handler, and when a Promise that was previously rejected * without a handler gets a handler attached. */ extern JS_PUBLIC_API void SetPromiseRejectionTrackerCallback( JSContext* cx, PromiseRejectionTrackerCallback callback, void* data = nullptr); /** * Inform the runtime that the job queue is empty and the embedding is going to * execute its last promise job. The runtime may now choose to skip creating * promise jobs for asynchronous execution and instead continue execution * synchronously. More specifically, this optimization is used to skip the * standard job queuing behavior for `await` operations in async functions. * * This function may be called before executing the last job in the job queue. * When it was called, JobQueueMayNotBeEmpty must be called in order to restore * the default job queuing behavior before the embedding enqueues its next job * into the job queue. */ extern JS_PUBLIC_API void JobQueueIsEmpty(JSContext* cx); /** * Inform the runtime that job queue is no longer empty. The runtime can now no * longer skip creating promise jobs for asynchronous execution, because * pending jobs in the job queue must be executed first to preserve the FIFO * (first in - first out) property of the queue. This effectively undoes * JobQueueIsEmpty and re-enables the standard job queuing behavior. * * This function must be called whenever enqueuing a job to the job queue when * JobQueueIsEmpty was called previously. */ extern JS_PUBLIC_API void JobQueueMayNotBeEmpty(JSContext* cx); /** * Returns a new instance of the Promise builtin class in the current * compartment, with the right slot layout. * * The `executor` can be a `nullptr`. In that case, the only way to resolve or * reject the returned promise is via the `JS::ResolvePromise` and * `JS::RejectPromise` JSAPI functions. */ extern JS_PUBLIC_API JSObject* NewPromiseObject(JSContext* cx, JS::HandleObject executor); /** * Returns true if the given object is an unwrapped PromiseObject, false * otherwise. */ extern JS_PUBLIC_API bool IsPromiseObject(JS::HandleObject obj); /** * Returns the current compartment's original Promise constructor. */ extern JS_PUBLIC_API JSObject* GetPromiseConstructor(JSContext* cx); /** * Returns the current compartment's original Promise.prototype. */ extern JS_PUBLIC_API JSObject* GetPromisePrototype(JSContext* cx); // Keep this in sync with the PROMISE_STATE defines in SelfHostingDefines.h. enum class PromiseState { Pending, Fulfilled, Rejected }; /** * Returns the given Promise's state as a JS::PromiseState enum value. * * Returns JS::PromiseState::Pending if the given object is a wrapper that * can't safely be unwrapped. */ extern JS_PUBLIC_API PromiseState GetPromiseState(JS::HandleObject promise); /** * Returns the given Promise's process-unique ID. */ JS_PUBLIC_API uint64_t GetPromiseID(JS::HandleObject promise); /** * Returns the given Promise's result: either the resolution value for * fulfilled promises, or the rejection reason for rejected ones. */ extern JS_PUBLIC_API JS::Value GetPromiseResult(JS::HandleObject promise); /** * Returns whether the given promise's rejection is already handled or not. * * The caller must check the given promise is rejected before checking it's * handled or not. */ extern JS_PUBLIC_API bool GetPromiseIsHandled(JS::HandleObject promise); /* * Given a settled (i.e. fulfilled or rejected, not pending) promise, sets * |promise.[[PromiseIsHandled]]| to true and removes it from the list of * unhandled rejected promises. */ extern JS_PUBLIC_API bool SetSettledPromiseIsHandled(JSContext* cx, JS::HandleObject promise); /* * Given a promise (settled or not), sets |promise.[[PromiseIsHandled]]| to true * and removes it from the list of unhandled rejected promises if it's settled. */ [[nodiscard]] extern JS_PUBLIC_API bool SetAnyPromiseIsHandled( JSContext* cx, JS::HandleObject promise); /** * Returns a js::SavedFrame linked list of the stack that lead to the given * Promise's allocation. */ extern JS_PUBLIC_API JSObject* GetPromiseAllocationSite( JS::HandleObject promise); extern JS_PUBLIC_API JSObject* GetPromiseResolutionSite( JS::HandleObject promise); #ifdef DEBUG extern JS_PUBLIC_API void DumpPromiseAllocationSite(JSContext* cx, JS::HandleObject promise); extern JS_PUBLIC_API void DumpPromiseResolutionSite(JSContext* cx, JS::HandleObject promise); #endif /** * Calls the current compartment's original Promise.resolve on the original * Promise constructor, with `resolutionValue` passed as an argument. */ extern JS_PUBLIC_API JSObject* CallOriginalPromiseResolve( JSContext* cx, JS::HandleValue resolutionValue); /** * Calls the current compartment's original Promise.reject on the original * Promise constructor, with `resolutionValue` passed as an argument. */ extern JS_PUBLIC_API JSObject* CallOriginalPromiseReject( JSContext* cx, JS::HandleValue rejectionValue); /** * Resolves the given Promise with the given `resolutionValue`. * * Calls the `resolve` function that was passed to the executor function when * the Promise was created. */ extern JS_PUBLIC_API bool ResolvePromise(JSContext* cx, JS::HandleObject promiseObj, JS::HandleValue resolutionValue); /** * Rejects the given `promise` with the given `rejectionValue`. * * Calls the `reject` function that was passed to the executor function when * the Promise was created. */ extern JS_PUBLIC_API bool RejectPromise(JSContext* cx, JS::HandleObject promiseObj, JS::HandleValue rejectionValue); /** * Create a Promise with the given fulfill/reject handlers, that will be * fulfilled/rejected with the value/reason that the promise `promise` is * fulfilled/rejected with. * * This function basically acts like `promise.then(onFulfilled, onRejected)`, * except that its behavior is unaffected by changes to `Promise`, * `Promise[Symbol.species]`, `Promise.prototype.then`, `promise.constructor`, * `promise.then`, and so on. * * This function throws if `promise` is not a Promise from this or another * realm. * * This function will assert if `onFulfilled` or `onRejected` is non-null and * also not IsCallable. */ extern JS_PUBLIC_API JSObject* CallOriginalPromiseThen( JSContext* cx, JS::HandleObject promise, JS::HandleObject onFulfilled, JS::HandleObject onRejected); /** * Unforgeable, optimized version of the JS builtin Promise.prototype.then. * * Takes a Promise instance and nullable `onFulfilled`/`onRejected` callables to * enqueue as reactions for that promise. In contrast to Promise.prototype.then, * this doesn't create and return a new Promise instance. * * Throws a TypeError if `promise` isn't a Promise (or possibly a different * error if it's a security wrapper or dead object proxy). */ extern JS_PUBLIC_API bool AddPromiseReactions(JSContext* cx, JS::HandleObject promise, JS::HandleObject onFulfilled, JS::HandleObject onRejected); /** * Unforgeable, optimized version of the JS builtin Promise.prototype.then. * * Takes a Promise instance and nullable `onFulfilled`/`onRejected` callables to * enqueue as reactions for that promise. In contrast to Promise.prototype.then, * this doesn't create and return a new Promise instance. * * Throws a TypeError if `promise` isn't a Promise (or possibly a different * error if it's a security wrapper or dead object proxy). * * If `onRejected` is null and `promise` is rejected, this function -- unlike * the function above -- will not report an unhandled rejection. */ extern JS_PUBLIC_API bool AddPromiseReactionsIgnoringUnhandledRejection( JSContext* cx, JS::HandleObject promise, JS::HandleObject onFulfilled, JS::HandleObject onRejected); // This enum specifies whether a promise is expected to keep track of // information that is useful for embedders to implement user activation // behavior handling as specified in the HTML spec: // https://html.spec.whatwg.org/multipage/interaction.html#triggered-by-user-activation // By default, promises created by SpiderMonkey do not make any attempt to keep // track of information about whether an activation behavior was being processed // when the original promise in a promise chain was created. If the embedder // sets either of the HadUserInteractionAtCreation or // DidntHaveUserInteractionAtCreation flags on a promise after creating it, // SpiderMonkey will propagate that flag to newly created promises when // processing Promise#then and will make it possible to query this flag off of a // promise further down the chain later using the // GetPromiseUserInputEventHandlingState() API. enum class PromiseUserInputEventHandlingState { // Don't keep track of this state (default for all promises) DontCare, // Keep track of this state, the original promise in the chain was created // while an activation behavior was being processed. HadUserInteractionAtCreation, // Keep track of this state, the original promise in the chain was created // while an activation behavior was not being processed. DidntHaveUserInteractionAtCreation }; /** * Returns the given Promise's activation behavior state flag per above as a * JS::PromiseUserInputEventHandlingState value. All promises are created with * the DontCare state by default. * * Returns JS::PromiseUserInputEventHandlingState::DontCare if the given object * is a wrapper that can't safely be unwrapped. */ extern JS_PUBLIC_API PromiseUserInputEventHandlingState GetPromiseUserInputEventHandlingState(JS::HandleObject promise); /** * Sets the given Promise's activation behavior state flag per above as a * JS::PromiseUserInputEventHandlingState value. * * Returns false if the given object is a wrapper that can't safely be * unwrapped. */ extern JS_PUBLIC_API bool SetPromiseUserInputEventHandlingState( JS::HandleObject promise, JS::PromiseUserInputEventHandlingState state); /** * Unforgeable version of the JS builtin Promise.all. * * Takes a HandleObjectVector of Promise objects and returns a promise that's * resolved with an array of resolution values when all those promises have * been resolved, or rejected with the rejection value of the first rejected * promise. * * Asserts that all objects in the `promises` vector are, maybe wrapped, * instances of `Promise` or a subclass of `Promise`. */ extern JS_PUBLIC_API JSObject* GetWaitForAllPromise( JSContext* cx, JS::HandleObjectVector promises); /** * The Dispatchable interface allows the embedding to call SpiderMonkey * on a JSContext thread when requested via DispatchToEventLoopCallback. */ class JS_PUBLIC_API Dispatchable { protected: // Dispatchables are created and destroyed by SpiderMonkey. Dispatchable() = default; virtual ~Dispatchable() = default; public: // ShuttingDown indicates that SpiderMonkey should abort async tasks to // expedite shutdown. enum MaybeShuttingDown { NotShuttingDown, ShuttingDown }; // Called by the embedding after DispatchToEventLoopCallback succeeds. virtual void run(JSContext* cx, MaybeShuttingDown maybeShuttingDown) = 0; }; /** * Callback to dispatch a JS::Dispatchable to a JSContext's thread's event loop. * * The DispatchToEventLoopCallback set on a particular JSContext must accept * JS::Dispatchable instances and arrange for their `run` methods to be called * eventually on the JSContext's thread. This is used for cross-thread dispatch, * so the callback itself must be safe to call from any thread. * * If the callback returns `true`, it must eventually run the given * Dispatchable; otherwise, SpiderMonkey may leak memory or hang. * * The callback may return `false` to indicate that the JSContext's thread is * shutting down and is no longer accepting runnables. Shutting down is a * one-way transition: once the callback has rejected a runnable, it must reject * all subsequently submitted runnables as well. * * To establish a DispatchToEventLoopCallback, the embedding may either call * InitDispatchToEventLoop to provide its own, or call js::UseInternalJobQueues * to select a default implementation built into SpiderMonkey. This latter * depends on the embedding to call js::RunJobs on the JavaScript thread to * process queued Dispatchables at appropriate times. */ typedef bool (*DispatchToEventLoopCallback)(void* closure, Dispatchable* dispatchable); extern JS_PUBLIC_API void InitDispatchToEventLoop( JSContext* cx, DispatchToEventLoopCallback callback, void* closure); /** * When a JSRuntime is destroyed it implicitly cancels all async tasks in * progress, releasing any roots held by the task. However, this is not soon * enough for cycle collection, which needs to have roots dropped earlier so * that the cycle collector can transitively remove roots for a future GC. For * these and other cases, the set of pending async tasks can be canceled * with this call earlier than JSRuntime destruction. */ extern JS_PUBLIC_API void ShutdownAsyncTasks(JSContext* cx); } // namespace JS #endif // js_Promise_h