Skip to content

Commit 54ab06e

Browse files
add governor combinators
1 parent c206a5e commit 54ab06e

File tree

2 files changed

+81
-2
lines changed

2 files changed

+81
-2
lines changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ The Governor name is taken from [the speed-limiting device in motor vehicles](ht
3434

3535
<details>
3636
<summary>
37-
There is also a Governor constructor with helpers on its prototype.
37+
There is also a Governor constructor with helpers.
3838
</summary>
3939

4040
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 => {
7070
```
7171

7272
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.
73+
74+
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.
7375
</details>
7476

7577
#### Open Questions
7678

77-
- should the protocol be Symbol-based?
79+
- should the acquire protocol be Symbol-based?
7880
- maybe a sync/throwing acquire?
7981
- `tryAcquire(): GovernorToken`
8082
- or maybe not throwing? `tryAcquire(): GovernorToken | null`
@@ -83,6 +85,9 @@ Similarly, `wrapIterator(it: Iterator<T> | AsyncIterator<T>): AsyncIterator<T>`
8385
- also takes a `tryAcquire` function?
8486
- easy enough to live without it
8587
- alternative name: Regulator?
88+
- it's kind of annoying to implement both "release" and Symbol.dispose
89+
- should any/all take an iterable instead of varargs to match Promise.any/all?
90+
- should "any" be named "race" since it better matches Promise.race?
8691

8792
### CountingGovernor
8893

src/governor.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,80 @@ export abstract class Governor {
3131

3232
// wrapIterable<T>(iter: Iterable<T> | AsyncIterable<T>): AsyncIterable<T> {
3333
// }
34+
35+
static all(...governors: Governor[]): Governor {
36+
return new ComposedGovernorAll(governors);
37+
}
38+
39+
static any(...governors: Governor[]): Governor {
40+
return new ComposedGovernorAny(governors);
41+
}
42+
}
43+
44+
class ComposedGovernorAll extends Governor {
45+
#governors;
46+
47+
constructor(governors: Governor[]) {
48+
super();
49+
this.#governors = governors
50+
}
51+
52+
async acquire(): Promise<GovernorToken> {
53+
let tokens = await Promise.all(this.#governors.map(g => g.acquire()));
54+
function dispose() {
55+
let deferred = null;
56+
for (let t of tokens) {
57+
try {
58+
t.release();
59+
} catch (e) {
60+
deferred ??= e;
61+
}
62+
};
63+
if (deferred) {
64+
throw deferred;
65+
}
66+
}
67+
return {
68+
release: dispose,
69+
[Symbol.dispose]: dispose,
70+
} as GovernorToken;
71+
}
72+
}
73+
74+
class ComposedGovernorAny extends Governor {
75+
#governors;
76+
77+
constructor(governors: Governor[]) {
78+
super();
79+
this.#governors = governors
80+
}
81+
82+
acquire(): Promise<GovernorToken> {
83+
// Governor.any()
84+
if (this.#governors.length === 0) {
85+
return Promise.resolve({
86+
release: () => {},
87+
[Symbol.dispose]: () => {},
88+
});
89+
}
90+
let settled = false;
91+
let { promise, resolve, reject } = Promise.withResolvers<GovernorToken>();
92+
let tokenPromises = this.#governors.map(g => g.acquire());
93+
for (let p of tokenPromises) {
94+
p.then(token => {
95+
if (!settled) {
96+
settled = true;
97+
resolve(token);
98+
} else {
99+
token.release();
100+
}
101+
});
102+
};
103+
Promise.any(tokenPromises).catch(e => {
104+
reject(e);
105+
});
106+
return promise;
107+
}
34108
}
35109

36110
export interface GovernorToken {

0 commit comments

Comments
 (0)