Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add governor combinators #9

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The Governor name is taken from [the speed-limiting device in motor vehicles](ht

<details>
<summary>
There is also a Governor constructor with helpers on its prototype.
There is also a Governor constructor with helpers.
</summary>

The constructor unconditionally throws when it is the `new.target`. To make the helpers available, a concrete Governor can be implemented as follows:
Expand Down Expand Up @@ -70,11 +70,13 @@ Governor.prototype.wrap = fn => {
```

Similarly, `wrapIterator(it: Iterator<T> | AsyncIterator<T>): AsyncIterator<T>` 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.
michaelficarra marked this conversation as resolved.
Show resolved Hide resolved
</details>

#### 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`
Expand All @@ -83,6 +85,9 @@ Similarly, `wrapIterator(it: Iterator<T> | AsyncIterator<T>): AsyncIterator<T>`
- 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?
ljharb marked this conversation as resolved.
Show resolved Hide resolved

### CountingGovernor

Expand Down
73 changes: 73 additions & 0 deletions src/governor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,79 @@ export abstract class Governor {

// wrapIterable<T>(iter: Iterable<T> | AsyncIterable<T>): AsyncIterable<T> {
// }

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<GovernorToken> {
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[]) {
// 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<GovernorToken> {
let settled = false;
let { promise, resolve, reject } = Promise.withResolvers<GovernorToken>();
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 an AggregateError
Promise.any(tokenPromises).catch(e => {
reject(e);
});
return promise;
}
}

export interface GovernorToken {
Expand Down