@mark1russell7/collections 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.ts +29 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +55 -1
- package/build/index.js.map +1 -1
- package/package.json +2 -1
- package/src/async/semaphore.ts +88 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/collections",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.4",
|
|
5
5
|
"description": "Collections with no dependencies: lists, maps, queues, a summary tree, channels, lazy operations and dependency graphs.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"collections",
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"@mark1russell7/rigor-statistics": "^0.1.8",
|
|
78
78
|
"@mark1russell7/rigor-testkit": "^0.1.0",
|
|
79
79
|
"@types/node": "^26.6.5",
|
|
80
|
+
"@vitest/coverage-v8": "^5.1.0",
|
|
80
81
|
"esbuild": "^0.28.2",
|
|
81
82
|
"fast-check": "^4.10.2",
|
|
82
83
|
"tsdown": "^0.23.1",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* semaphore - A bound on the tasks that run at the same time.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* const writes = semaphore(4);
|
|
6
|
+
* // At most 4 writes run at once. The others wait in the order that they came.
|
|
7
|
+
* await Promise.all(items.map((item) => writes.run(() => store.set(item.id, item))));
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** A semaphore: a count of permits, and a queue of the callers that wait for one. */
|
|
11
|
+
export interface Semaphore {
|
|
12
|
+
/** The permits of the semaphore. */
|
|
13
|
+
readonly permits: number;
|
|
14
|
+
/** The permits that no caller holds. */
|
|
15
|
+
readonly available: number;
|
|
16
|
+
/** The callers that wait for a permit. */
|
|
17
|
+
readonly waiting: number;
|
|
18
|
+
/**
|
|
19
|
+
* This method gives a permit. It waits while all permits are held. The waiting callers get the permits in the order
|
|
20
|
+
* that they came. The result is the release of the permit. A second call of the release does nothing.
|
|
21
|
+
*/
|
|
22
|
+
acquire(): Promise<() => void>;
|
|
23
|
+
/** This method starts a task with a permit. The permit goes back when the task ends, also when it throws. */
|
|
24
|
+
run<R>(task: () => R | PromiseLike<R>): Promise<R>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* This function makes a semaphore with `permits` permits. The value is a whole number of 1 or more, or `Infinity`
|
|
29
|
+
* for no bound. Another value throws a `RangeError`.
|
|
30
|
+
*/
|
|
31
|
+
export function semaphore(permits: number): Semaphore {
|
|
32
|
+
if (!(permits === Infinity || (Number.isInteger(permits) && permits >= 1))) {
|
|
33
|
+
throw new RangeError(`A semaphore needs a whole number of permits of 1 or more, or Infinity: ${permits}`);
|
|
34
|
+
}
|
|
35
|
+
let held = 0;
|
|
36
|
+
// The waiting callers, in the order that they came. A release gives its permit to the first.
|
|
37
|
+
const queue: Array<() => void> = [];
|
|
38
|
+
let head = 0;
|
|
39
|
+
|
|
40
|
+
const releaseOnce = (): (() => void) => {
|
|
41
|
+
let done = false;
|
|
42
|
+
return () => {
|
|
43
|
+
if (done) return;
|
|
44
|
+
done = true;
|
|
45
|
+
if (head < queue.length) {
|
|
46
|
+
const next = queue[head]!;
|
|
47
|
+
head += 1;
|
|
48
|
+
// The permit goes from the releaser to the next caller, so `held` does not change
|
|
49
|
+
if (head > 64 && head * 2 > queue.length) {
|
|
50
|
+
queue.splice(0, head);
|
|
51
|
+
head = 0;
|
|
52
|
+
}
|
|
53
|
+
next();
|
|
54
|
+
} else {
|
|
55
|
+
held -= 1;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const acquire = (): Promise<() => void> => {
|
|
61
|
+
if (held < permits) {
|
|
62
|
+
held += 1;
|
|
63
|
+
return Promise.resolve(releaseOnce());
|
|
64
|
+
}
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
queue.push(() => resolve(releaseOnce()));
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
permits,
|
|
72
|
+
get available() {
|
|
73
|
+
return permits - held;
|
|
74
|
+
},
|
|
75
|
+
get waiting() {
|
|
76
|
+
return queue.length - head;
|
|
77
|
+
},
|
|
78
|
+
acquire,
|
|
79
|
+
async run<R>(task: () => R | PromiseLike<R>): Promise<R> {
|
|
80
|
+
const release = await acquire();
|
|
81
|
+
try {
|
|
82
|
+
return await task();
|
|
83
|
+
} finally {
|
|
84
|
+
release();
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -93,6 +93,7 @@ export * from "./behaviors/ttl.js";
|
|
|
93
93
|
export * from "./async/async-queue.js";
|
|
94
94
|
export * from "./async/channels.js";
|
|
95
95
|
export * from "./async/settled.js";
|
|
96
|
+
export * from "./async/semaphore.js";
|
|
96
97
|
|
|
97
98
|
// ============================================================================
|
|
98
99
|
// Functional Operations
|
|
@@ -135,6 +136,7 @@ export { ttlMap, ttlCache, TTLCache, ttlCollection } from "./behaviors/ttl.js";
|
|
|
135
136
|
// Async/Concurrent
|
|
136
137
|
export { asyncQueue, AsyncQueue } from "./async/async-queue.js";
|
|
137
138
|
export { asSettled, type Settled } from "./async/settled.js";
|
|
139
|
+
export { semaphore, type Semaphore } from "./async/semaphore.js";
|
|
138
140
|
export {
|
|
139
141
|
channel,
|
|
140
142
|
Channel,
|