Skip to content

Commit 6eaad95

Browse files
authored
Merge pull request #29 from jsr-core/refactor
feat: throw `RangeError` on invalid arguments and refactor internal impl of `Notify`
2 parents b000238 + f3db52a commit 6eaad95

10 files changed

+191
-29
lines changed

barrier.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ export class Barrier {
3333
* Creates a new `Barrier` that blocks until `size` threads have called `wait`.
3434
*
3535
* @param size The number of threads that must reach the barrier before it unblocks.
36-
* @throws Error if the size is negative.
36+
* @throws {RangeError} if the size is not a positive safe integer.
3737
*/
3838
constructor(size: number) {
39-
if (size < 0) {
40-
throw new Error("The size must be greater than 0");
39+
if (size <= 0 || !Number.isSafeInteger(size)) {
40+
throw new RangeError(
41+
`size must be a positive safe integer, got ${size}`,
42+
);
4143
}
4244
this.#rest = size;
4345
}

barrier_test.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { assertEquals, assertRejects } from "@std/assert";
1+
import { assertEquals, assertRejects, assertThrows } from "@std/assert";
22
import { deadline, delay } from "@std/async";
33
import { Barrier } from "./barrier.ts";
44

@@ -79,4 +79,16 @@ Deno.test("Barrier", async (t) => {
7979
);
8080
},
8181
);
82+
83+
await t.step(
84+
"throws RangeError if size is not a positive safe integer",
85+
() => {
86+
assertThrows(() => new Barrier(NaN), RangeError);
87+
assertThrows(() => new Barrier(Infinity), RangeError);
88+
assertThrows(() => new Barrier(-Infinity), RangeError);
89+
assertThrows(() => new Barrier(-1), RangeError);
90+
assertThrows(() => new Barrier(1.1), RangeError);
91+
assertThrows(() => new Barrier(0), RangeError);
92+
},
93+
);
8294
});

deno.jsonc

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"@core/asyncutil/semaphore": "./semaphore.ts",
4343
"@core/asyncutil/stack": "./stack.ts",
4444
"@core/asyncutil/wait-group": "./wait_group.ts",
45+
"@core/iterutil": "jsr:@core/iterutil@^0.6.0",
4546
"@std/assert": "jsr:@std/assert@^1.0.2",
4647
"@std/async": "jsr:@std/async@^1.0.2"
4748
},

deno.lock

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

notify.ts

+17-18
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { iter } from "@core/iterutil/iter";
2+
import { take } from "@core/iterutil/take";
3+
14
/**
25
* Async notifier that allows one or more "waiters" to wait for a notification.
36
*
@@ -20,30 +23,31 @@
2023
* ```
2124
*/
2225
export class Notify {
23-
#waiters: {
24-
promise: Promise<void>;
25-
resolve: () => void;
26-
reject: (reason?: unknown) => void;
27-
}[] = [];
26+
#waiters: Set<PromiseWithResolvers<void>> = new Set();
2827

2928
/**
3029
* Returns the number of waiters that are waiting for notification.
3130
*/
3231
get waiterCount(): number {
33-
return this.#waiters.length;
32+
return this.#waiters.size;
3433
}
3534

3635
/**
3736
* Notifies `n` waiters that are waiting for notification. Resolves each of the notified waiters.
3837
* If there are fewer than `n` waiters, all waiters are notified.
38+
*
39+
* @param n The number of waiters to notify.
40+
* @throws {RangeError} if `n` is not a positive safe integer.
3941
*/
4042
notify(n = 1): void {
41-
const head = this.#waiters.slice(0, n);
42-
const tail = this.#waiters.slice(n);
43-
for (const waiter of head) {
43+
if (n <= 0 || !Number.isSafeInteger(n)) {
44+
throw new RangeError(`n must be a positive safe integer, got ${n}`);
45+
}
46+
const it = iter(this.#waiters);
47+
for (const waiter of take(it, n)) {
4448
waiter.resolve();
4549
}
46-
this.#waiters = tail;
50+
this.#waiters = new Set(it);
4751
}
4852

4953
/**
@@ -53,7 +57,7 @@ export class Notify {
5357
for (const waiter of this.#waiters) {
5458
waiter.resolve();
5559
}
56-
this.#waiters = [];
60+
this.#waiters = new Set();
5761
}
5862

5963
/**
@@ -67,17 +71,12 @@ export class Notify {
6771
}
6872
const waiter = Promise.withResolvers<void>();
6973
const abort = () => {
70-
removeItem(this.#waiters, waiter);
74+
this.#waiters.delete(waiter);
7175
waiter.reject(signal!.reason);
7276
};
7377
signal?.addEventListener("abort", abort, { once: true });
74-
this.#waiters.push(waiter);
78+
this.#waiters.add(waiter);
7579
await waiter.promise;
7680
signal?.removeEventListener("abort", abort);
7781
}
7882
}
79-
80-
function removeItem<T>(array: T[], item: T): void {
81-
const index = array.indexOf(item);
82-
array.splice(index, 1);
83-
}

notify_test.ts

+54-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { delay } from "@std/async/delay";
2-
import { assertEquals, assertRejects } from "@std/assert";
2+
import { assertEquals, assertRejects, assertThrows } from "@std/assert";
33
import { promiseState } from "./promise_state.ts";
44
import { Notify } from "./notify.ts";
55

@@ -8,21 +8,61 @@ Deno.test("Notify", async (t) => {
88
const notify = new Notify();
99
const waiter1 = notify.notified();
1010
const waiter2 = notify.notified();
11+
assertEquals(notify.waiterCount, 2);
1112

1213
notify.notify();
14+
assertEquals(notify.waiterCount, 1);
1315
assertEquals(await promiseState(waiter1), "fulfilled");
1416
assertEquals(await promiseState(waiter2), "pending");
1517

1618
notify.notify();
19+
assertEquals(notify.waiterCount, 0);
1720
assertEquals(await promiseState(waiter1), "fulfilled");
1821
assertEquals(await promiseState(waiter2), "fulfilled");
1922
});
2023

24+
await t.step("'notify' wakes up a multiple waiters", async () => {
25+
const notify = new Notify();
26+
const waiter1 = notify.notified();
27+
const waiter2 = notify.notified();
28+
const waiter3 = notify.notified();
29+
const waiter4 = notify.notified();
30+
const waiter5 = notify.notified();
31+
assertEquals(notify.waiterCount, 5);
32+
33+
notify.notify(2);
34+
assertEquals(notify.waiterCount, 3);
35+
assertEquals(await promiseState(waiter1), "fulfilled");
36+
assertEquals(await promiseState(waiter2), "fulfilled");
37+
assertEquals(await promiseState(waiter3), "pending");
38+
assertEquals(await promiseState(waiter4), "pending");
39+
assertEquals(await promiseState(waiter5), "pending");
40+
41+
notify.notify(2);
42+
assertEquals(notify.waiterCount, 1);
43+
assertEquals(await promiseState(waiter1), "fulfilled");
44+
assertEquals(await promiseState(waiter2), "fulfilled");
45+
assertEquals(await promiseState(waiter3), "fulfilled");
46+
assertEquals(await promiseState(waiter4), "fulfilled");
47+
assertEquals(await promiseState(waiter5), "pending");
48+
49+
notify.notify(2);
50+
assertEquals(notify.waiterCount, 0);
51+
assertEquals(await promiseState(waiter1), "fulfilled");
52+
assertEquals(await promiseState(waiter2), "fulfilled");
53+
assertEquals(await promiseState(waiter3), "fulfilled");
54+
assertEquals(await promiseState(waiter4), "fulfilled");
55+
assertEquals(await promiseState(waiter5), "fulfilled");
56+
});
57+
2158
await t.step("'notifyAll' wakes up all waiters", async () => {
2259
const notify = new Notify();
2360
const waiter1 = notify.notified();
2461
const waiter2 = notify.notified();
62+
assertEquals(notify.waiterCount, 2);
63+
2564
notify.notifyAll();
65+
assertEquals(notify.waiterCount, 0);
2666
assertEquals(await promiseState(waiter1), "fulfilled");
2767
assertEquals(await promiseState(waiter2), "fulfilled");
2868
});
@@ -69,4 +109,17 @@ Deno.test("Notify", async (t) => {
69109
);
70110
},
71111
);
112+
113+
await t.step(
114+
"'notify' throws RangeError if size is not a positive safe integer",
115+
() => {
116+
const notify = new Notify();
117+
assertThrows(() => notify.notify(NaN), RangeError);
118+
assertThrows(() => notify.notify(Infinity), RangeError);
119+
assertThrows(() => notify.notify(-Infinity), RangeError);
120+
assertThrows(() => notify.notify(-1), RangeError);
121+
assertThrows(() => notify.notify(1.1), RangeError);
122+
assertThrows(() => notify.notify(0), RangeError);
123+
},
124+
);
72125
});

semaphore.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ export class Semaphore {
2323
* Creates a new semaphore with the specified limit.
2424
*
2525
* @param size The maximum number of times the semaphore can be acquired before blocking.
26-
* @throws Error if the size is less than 1.
26+
* @throws {RangeError} if the size is not a positive safe integer.
2727
*/
2828
constructor(size: number) {
29-
if (size < 0) {
30-
throw new Error("The size must be greater than 0");
29+
if (size <= 0 || !Number.isSafeInteger(size)) {
30+
throw new RangeError(
31+
`size must be a positive safe integer, got ${size}`,
32+
);
3133
}
3234
this.#rest = size + 1;
3335
}

semaphore_test.ts

+76-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { assertEquals } from "@std/assert";
1+
import { assertEquals, assertThrows } from "@std/assert";
22
import { Semaphore } from "./semaphore.ts";
33

44
Deno.test("Semaphore", async (t) => {
55
await t.step(
6-
"regulates the number of workers concurrently running",
6+
"regulates the number of workers concurrently running (n=5)",
77
async () => {
88
let nworkers = 0;
99
const results: number[] = [];
@@ -32,4 +32,78 @@ Deno.test("Semaphore", async (t) => {
3232
]);
3333
},
3434
);
35+
36+
await t.step(
37+
"regulates the number of workers concurrently running (n=1)",
38+
async () => {
39+
let nworkers = 0;
40+
const results: number[] = [];
41+
const sem = new Semaphore(1);
42+
const worker = () => {
43+
return sem.lock(async () => {
44+
nworkers++;
45+
results.push(nworkers);
46+
await new Promise((resolve) => setTimeout(resolve, 10));
47+
nworkers--;
48+
});
49+
};
50+
await Promise.all([...Array(10)].map(() => worker()));
51+
assertEquals(nworkers, 0);
52+
assertEquals(results, [
53+
1,
54+
1,
55+
1,
56+
1,
57+
1,
58+
1,
59+
1,
60+
1,
61+
1,
62+
1,
63+
]);
64+
},
65+
);
66+
67+
await t.step(
68+
"regulates the number of workers concurrently running (n=10)",
69+
async () => {
70+
let nworkers = 0;
71+
const results: number[] = [];
72+
const sem = new Semaphore(10);
73+
const worker = () => {
74+
return sem.lock(async () => {
75+
nworkers++;
76+
results.push(nworkers);
77+
await new Promise((resolve) => setTimeout(resolve, 10));
78+
nworkers--;
79+
});
80+
};
81+
await Promise.all([...Array(10)].map(() => worker()));
82+
assertEquals(nworkers, 0);
83+
assertEquals(results, [
84+
1,
85+
2,
86+
3,
87+
4,
88+
5,
89+
6,
90+
7,
91+
8,
92+
9,
93+
10,
94+
]);
95+
},
96+
);
97+
98+
await t.step(
99+
"throws RangeError if size is not a positive safe integer",
100+
() => {
101+
assertThrows(() => new Semaphore(NaN), RangeError);
102+
assertThrows(() => new Semaphore(Infinity), RangeError);
103+
assertThrows(() => new Semaphore(-Infinity), RangeError);
104+
assertThrows(() => new Semaphore(-1), RangeError);
105+
assertThrows(() => new Semaphore(1.1), RangeError);
106+
assertThrows(() => new Semaphore(0), RangeError);
107+
},
108+
);
35109
});

wait_group.ts

+3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export class WaitGroup {
3636
* @param delta The number to add to the counter. It can be positive or negative.
3737
*/
3838
add(delta: number): void {
39+
if (!Number.isSafeInteger(delta)) {
40+
throw new RangeError(`delta must be a safe integer, got ${delta}`);
41+
}
3942
this.#count += delta;
4043
if (this.#count === 0) {
4144
this.#notify.notifyAll();

wait_group_test.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { assertEquals, assertRejects } from "@std/assert";
1+
import { assertEquals, assertRejects, assertThrows } from "@std/assert";
22
import { deadline, delay } from "@std/async";
33
import { WaitGroup } from "./wait_group.ts";
44

@@ -83,4 +83,15 @@ Deno.test("WaitGroup", async (t) => {
8383
);
8484
},
8585
);
86+
87+
await t.step(
88+
"'add' throws RangeError if delta is not a safe integer",
89+
() => {
90+
const wg = new WaitGroup();
91+
assertThrows(() => wg.add(NaN), RangeError);
92+
assertThrows(() => wg.add(Infinity), RangeError);
93+
assertThrows(() => wg.add(-Infinity), RangeError);
94+
assertThrows(() => wg.add(1.1), RangeError);
95+
},
96+
);
8697
});

0 commit comments

Comments
 (0)