Stage: 1
Champions: Michael Ficarra, Luca Casonato
Authors: Michael Ficarra, Luca Casonato, Kevin Gibbons
This proposal aims to provide a mechanism for describing a desired amount of concurrency and a coordination mechanism to achieve it. This could be for limiting concurrent access to a shared resource or for setting a target concurrency for an otherwise unbounded workload.
A major motivator for this proposal is the concurrency support in the async iterator helpers proposal. While that proposal has gone to great lengths to allow for concurrent iteration of its produced async iterators (such as through map
and filter
), it does not provide any way to consume async iterators concurrently (such as through some
or forEach
). Additionally, there is no mechanism provided by that proposal for generically limiting concurrent iteration of async iterators. This propsal attempts to address those deferred needs.
The concurrency control mechanism proposed here is also motivated by many other use cases outside of async iteration. For example, an application may want to limit the concurrency of a certain function being invoked, or limit concurrent file reads, writes, or network requests. This proposal aims to provide a generic mechanism for controlling concurrency in JavaScript that can be used in a wide variety of use cases.
This proposal consists of 3 major components: a Governor interface, the CountingGovernor class, and the AsyncIterator.prototype integration.
The Governor interface is used for gaining access to a limited resource and later signalling that you are finished with that resource. It is intentionally designed in a way that permits dynamically changing limits.
There is only a single method required by the Governor interface: acquire
, returning a Promise that eventually resolves with a GovernorToken
. A GovernorToken
has a release
method to indicate that the resource is no longer needed. The GovernorToken
can also be automatically disposed using using
syntax from the Explicit Resource Management proposal.
A Governor is meant to control access to resources among mutually trustworthy parties. For adversarial scenarios, a Capability should be used instead.
The Governor name is taken from the speed-limiting device in motor vehicles.
tryAcquire(): GovernorToken
tryAcquire(): GovernorToken | null
acquire: () => Promise<GovernorToken>
functiontryAcquire
function?This proposal subsumes Luca's Semaphore proposal.
CountingGovernor is a counting semaphore that implements the Governor interface and extends Governor. It can be given a non-negative integral Number capacity and it is responsible for ensuring that there are no more than that number of active GovernorTokens simultaneously.
addIdleListener(cb: () => void): void
removeIdleListener(cb: () => void): void
This proposal adds an optional concurrency parameter to the following async iterator helper methods:
.toArray([ governor ])
.forEach(fn [, governor ])
.some(predicate [, governor ])
.every(predicate [, governor ])
.find(predicate [, governor ])
.reduce(reducer [, initialValue [, governor ]])
When not passed, these methods operate serially, as they do in the async iterator helpers proposal.
This proposal also adds a limit(governor)
method (the dual of governor.wrapIterator(iterator)
) that returns a concurrency-limited AsyncIterator.
Because CountingGovernor will be an extremely commonly-used Governor, anywhere a Governor is accepted in any AsyncIterator.prototype method, a non-negative integral Number may be passed instead. It will be treated as if a CountingGovernor with that capacity was passed. Because of this, we are able to widen the first parameter of the buffered
helper to accept a Governor in addition to the non-negative integral Number that it accepts as part of the async iterator helpers proposal.
reduce
parameter order: gross?buffered
parameter orderacquire(): Promise<GovernorToken>
with(fn: () => R): Promise<R>
wrap(fn: (...args) => R): (...args) => Promise<R>
wrapIterator(it: Iterator<T> | AsyncIterator<T>): AsyncIterator<T>
release(): void
=== [Symbol.dispose](): void
CountingGovernor(capacity: number)
constructorbuffered(limit: Governor | integer, prepopulate = false)
limit(limit: Governor | integer)
Governor | integer
) added to all consuming methodsWhile a counting semaphore is a common concurrency control mechanism, there are many other ways to control concurrency. Some examples of these:
Because of this variety of use cases, it is important to give developers the flexibility to use their own concurrency control mechanisms with built in JavaScript APIs.
The CountingGovernor
class provides a simple and common concurrency control mechanism that can be used in many cases. It is expected that many developers will use CountingGovernor
for their concurrency control needs. However, because APIs don't explicitly take a CountingGovernor
instance, but any object that implements the Governor
interface, developers can use their own concurrency control mechanisms if they need to.