diff --git a/README.md b/README.md
index 01ff6a2..61e79d8 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ The Governor name is taken from [the speed-limiting device in motor vehicles](ht
-There is also a Governor constructor with helpers on its prototype.
+There is also a Governor constructor with helpers.
The constructor unconditionally throws when it is the `new.target`. To make the helpers available, a concrete Governor can be implemented as follows:
@@ -70,11 +70,13 @@ Governor.prototype.wrap = fn => {
```
Similarly, `wrapIterator(it: Iterator | AsyncIterator): AsyncIterator` takes an Iterator or AsyncIterator and returns an AsyncIterator that yields the same values but limited in concurrency by this Governor.
+
+There are also static helpers for composing Governors: `Governor.any` and `Governor.all`. `any` takes 0 or more Governors and produces a Governor that, when acquired, attempts to acquire all of the passed Governors, returns the first GovernorToken it receives, and releases any other tokens it acquires. `all` takes 0 or more Governors and produces a Governor that attempts to acquire all of the passed Governors and only returns a GovernorToken once it has received a token for each of them.
#### Open Questions
-- should the protocol be Symbol-based?
+- should the acquire protocol be Symbol-based?
- maybe a sync/throwing acquire?
- `tryAcquire(): GovernorToken`
- or maybe not throwing? `tryAcquire(): GovernorToken | null`
@@ -83,6 +85,9 @@ Similarly, `wrapIterator(it: Iterator | AsyncIterator): AsyncIterator`
- also takes a `tryAcquire` function?
- easy enough to live without it
- alternative name: Regulator?
+- it's kind of annoying to implement both "release" and Symbol.dispose
+- should any/all take an iterable instead of varargs to match Promise.any/all?
+- should "any" be named "race" since it better matches Promise.race?
### CountingGovernor
diff --git a/src/governor.ts b/src/governor.ts
index 2021919..fb328e4 100644
--- a/src/governor.ts
+++ b/src/governor.ts
@@ -31,6 +31,82 @@ export abstract class Governor {
// wrapIterable(iter: Iterable | AsyncIterable): AsyncIterable {
// }
+
+ static all(...governors: Governor[]): Governor {
+ return new ComposedGovernorAll(governors);
+ }
+
+ static any(...governors: Governor[]): Governor {
+ return new ComposedGovernorAny(governors);
+ }
+}
+
+class ComposedGovernorAll extends Governor {
+ #governors;
+
+ constructor(governors: Governor[]) {
+ super();
+ this.#governors = governors
+ }
+
+ async acquire(): Promise {
+ let tokens = await Promise.all(this.#governors.map(g => g.acquire()));
+ function dispose() {
+ let deferred = null;
+ for (let t of tokens) {
+ try {
+ t.release();
+ } catch (e) {
+ deferred ??= e;
+ }
+ };
+ if (deferred) {
+ throw deferred;
+ }
+ }
+ return {
+ release: dispose,
+ [Symbol.dispose]: dispose,
+ } as GovernorToken;
+ }
+}
+
+class ComposedGovernorAny extends Governor {
+ #governors;
+
+ constructor(governors: Governor[]) {
+ super();
+ this.#governors = governors
+ }
+
+ acquire(): Promise {
+ // Governor.any([]) should be infinitely acquire-able
+ if (this.#governors.length === 0) {
+ return Promise.resolve({
+ release: () => {},
+ [Symbol.dispose]: () => {},
+ });
+ }
+ let settled = false;
+ let { promise, resolve, reject } = Promise.withResolvers();
+ let tokenPromises = this.#governors.map(g => g.acquire());
+ // resolve with the first token we acquire, and insta-release all others
+ for (let p of tokenPromises) {
+ p.then(token => {
+ if (!settled) {
+ settled = true;
+ resolve(token);
+ } else {
+ token.release();
+ }
+ });
+ };
+ // if all tokenPromises reject, we should reject with the AggregateError
+ Promise.any(tokenPromises).catch(e => {
+ reject(e);
+ });
+ return promise;
+ }
}
export interface GovernorToken {