Skip to content

Commit

Permalink
add governor combinators
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelficarra committed Oct 25, 2024
1 parent c206a5e commit 54ab06e
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 2 deletions.
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.
</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?

### CountingGovernor

Expand Down
74 changes: 74 additions & 0 deletions src/governor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,80 @@ 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[]) {
super();
this.#governors = governors
}

acquire(): Promise<GovernorToken> {
// Governor.any()
if (this.#governors.length === 0) {
return Promise.resolve({
release: () => {},
[Symbol.dispose]: () => {},
});
}
let settled = false;
let { promise, resolve, reject } = Promise.withResolvers<GovernorToken>();
let tokenPromises = this.#governors.map(g => g.acquire());
for (let p of tokenPromises) {
p.then(token => {
if (!settled) {
settled = true;
resolve(token);
} else {
token.release();
}
});
};
Promise.any(tokenPromises).catch(e => {
reject(e);
});
return promise;
}
}

export interface GovernorToken {
Expand Down

0 comments on commit 54ab06e

Please sign in to comment.