From 2df4717989d85aeb925c9641f01b4d04c43adc5d Mon Sep 17 00:00:00 2001 From: Michael Ficarra Date: Thu, 24 Oct 2024 19:09:47 -0600 Subject: [PATCH 1/2] add governor combinators --- README.md | 9 ++++-- src/governor.ts | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) 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 { From 5832c40959376d1c511fa155ead95899f20ec5a4 Mon Sep 17 00:00:00 2001 From: Michael Ficarra Date: Fri, 25 Oct 2024 16:44:19 -0600 Subject: [PATCH 2/2] make Governor.any([]) throw immediately --- src/governor.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/governor.ts b/src/governor.ts index fb328e4..f42ebef 100644 --- a/src/governor.ts +++ b/src/governor.ts @@ -75,18 +75,15 @@ class ComposedGovernorAny extends Governor { #governors; constructor(governors: Governor[]) { + // Governor.any([]) would be impossible to acquire + if (governors.length === 0) { + throw new RangeError("at least one governor must be provided"); + } 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()); @@ -101,7 +98,7 @@ class ComposedGovernorAny extends Governor { } }); }; - // if all tokenPromises reject, we should reject with the AggregateError + // if all tokenPromises reject, we should reject with an AggregateError Promise.any(tokenPromises).catch(e => { reject(e); });