@icazemier/sway 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.md +90 -0
- package/build/cjs/adaptive-controller.d.ts +54 -0
- package/build/cjs/adaptive-controller.js +125 -0
- package/build/cjs/index.d.ts +3 -0
- package/build/cjs/index.js +7 -0
- package/build/cjs/interfaces.d.ts +41 -0
- package/build/cjs/interfaces.js +2 -0
- package/build/cjs/package.json +3 -0
- package/build/cjs/sway.d.ts +25 -0
- package/build/cjs/sway.js +84 -0
- package/build/esm/adaptive-controller.d.ts +54 -0
- package/build/esm/adaptive-controller.js +127 -0
- package/build/esm/index.d.ts +3 -0
- package/build/esm/index.js +2 -0
- package/build/esm/interfaces.d.ts +41 -0
- package/build/esm/interfaces.js +1 -0
- package/build/esm/package.json +3 -0
- package/build/esm/sway.d.ts +25 -0
- package/build/esm/sway.js +81 -0
- package/package.json +91 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Ivo Cazemier
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<img src="gibbons.png" width="200" />
|
|
2
|
+
|
|
3
|
+
# @icazemier/sway
|
|
4
|
+
|
|
5
|
+
`Promise.all()` with adaptive concurrency control. A gradient-based controller continuously measures throughput and adjusts the concurrency level to maximise task completion speed — zero dependencies.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @icazemier/sway
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { sway } from '@icazemier/sway';
|
|
17
|
+
|
|
18
|
+
const { results, stats } = await sway(
|
|
19
|
+
urls.map(url => () => fetch(url).then(r => r.json())),
|
|
20
|
+
{ maxConcurrency: 16 }
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
console.log(results); // resolved values in original order
|
|
24
|
+
console.log(stats.peakConcurrency); // highest concurrency reached
|
|
25
|
+
console.log(stats.avgConcurrency); // average concurrency across the run
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
Sway starts at `initialConcurrency` and probes every `probeInterval` completed tasks. Each probe measures throughput (tasks/sec) using an exponential moving average and compares it to the previous measurement:
|
|
31
|
+
|
|
32
|
+
- **Gradient > 0** — throughput improving, increase concurrency by 1
|
|
33
|
+
- **Gradient < 0** — throughput degrading, decrease concurrency by 1
|
|
34
|
+
- **Gradient = 0** — at optimum, hold steady
|
|
35
|
+
|
|
36
|
+
Concurrency is always clamped between `minConcurrency` and `maxConcurrency`. The `smoothingFactor` controls how responsive the EMA is to change (lower = calmer).
|
|
37
|
+
|
|
38
|
+
## Options
|
|
39
|
+
|
|
40
|
+
All values are **counts** or **ratios** — no time-based units.
|
|
41
|
+
|
|
42
|
+
| Option | Default | Unit | Description |
|
|
43
|
+
| -------------------- | ------- | ----- | --------------------------------------------- |
|
|
44
|
+
| `maxConcurrency` | `64` | tasks | Max concurrent in-flight tasks |
|
|
45
|
+
| `minConcurrency` | `1` | tasks | Min concurrent in-flight tasks |
|
|
46
|
+
| `initialConcurrency` | `4` | tasks | Concurrent in-flight tasks to start with |
|
|
47
|
+
| `smoothingFactor` | `0.3` | ratio | EMA smoothing (0–1), lower = calmer |
|
|
48
|
+
| `probeInterval` | `8` | tasks | Completed tasks between probe adjustments |
|
|
49
|
+
|
|
50
|
+
## Error handling
|
|
51
|
+
|
|
52
|
+
Sway rejects on the first error, just like `Promise.all()`.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
try {
|
|
56
|
+
await sway(tasks);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
// first task rejection
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Iterables
|
|
63
|
+
|
|
64
|
+
Accepts any `Iterable` — arrays, generators, or custom iterables. Tasks are pulled lazily from the iterator.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
function* generateTasks() {
|
|
68
|
+
for (const id of ids) {
|
|
69
|
+
yield () => processItem(id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { results } = await sway(generateTasks());
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Advanced: AdaptiveController
|
|
77
|
+
|
|
78
|
+
The controller is exported separately for custom integrations.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { AdaptiveController } from '@icazemier/sway';
|
|
82
|
+
|
|
83
|
+
const controller = new AdaptiveController({ maxConcurrency: 32 });
|
|
84
|
+
controller.getConcurrency(); // current level
|
|
85
|
+
controller.recordCompletion(); // signal a completed task
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { SwayOptions, SwayStats } from './interfaces.js';
|
|
2
|
+
/**
|
|
3
|
+
* Gradient-based concurrency controller.
|
|
4
|
+
*
|
|
5
|
+
* Measures throughput via an exponential moving average (EMA) and adjusts the
|
|
6
|
+
* concurrency level to maximise task completion speed. The controller probes
|
|
7
|
+
* every {@link SwayOptions.probeInterval | probeInterval} completed tasks,
|
|
8
|
+
* compares the current EMA throughput against the previous one, and nudges
|
|
9
|
+
* concurrency up or down by one depending on the gradient direction.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const controller = new AdaptiveController({ maxConcurrency: 16 });
|
|
14
|
+
* controller.getConcurrency(); // 4 (default initial)
|
|
15
|
+
* controller.recordCompletion();
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare class AdaptiveController {
|
|
19
|
+
private concurrency;
|
|
20
|
+
private readonly maxConcurrency;
|
|
21
|
+
private readonly minConcurrency;
|
|
22
|
+
private readonly smoothingFactor;
|
|
23
|
+
private readonly probeInterval;
|
|
24
|
+
private completionsSinceLastProbe;
|
|
25
|
+
private emaThroughput;
|
|
26
|
+
private previousEmaThroughput;
|
|
27
|
+
private lastProbeTime;
|
|
28
|
+
private peakConcurrency;
|
|
29
|
+
private concurrencySum;
|
|
30
|
+
private concurrencySamples;
|
|
31
|
+
private adjustmentCount;
|
|
32
|
+
/**
|
|
33
|
+
* @param options - Tuning knobs for the controller (all optional)
|
|
34
|
+
*/
|
|
35
|
+
constructor(options?: SwayOptions);
|
|
36
|
+
/**
|
|
37
|
+
* Signal that a task has completed. Triggers a probe when
|
|
38
|
+
* {@link SwayOptions.probeInterval | probeInterval} completions have accumulated.
|
|
39
|
+
*/
|
|
40
|
+
recordCompletion(): void;
|
|
41
|
+
/** Returns the current concurrency level. */
|
|
42
|
+
getConcurrency(): number;
|
|
43
|
+
/**
|
|
44
|
+
* Build a {@link SwayStats} snapshot.
|
|
45
|
+
*
|
|
46
|
+
* @param totalTasks - Total tasks executed
|
|
47
|
+
* @param totalDurationMs - Wall-clock duration of the run in milliseconds
|
|
48
|
+
* @returns Performance telemetry
|
|
49
|
+
*/
|
|
50
|
+
getStats(totalTasks: number, totalDurationMs: number): SwayStats;
|
|
51
|
+
private probe;
|
|
52
|
+
private setConcurrency;
|
|
53
|
+
private clamp;
|
|
54
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AdaptiveController = void 0;
|
|
4
|
+
const DEFAULT_MAX_CONCURRENCY = 64;
|
|
5
|
+
const DEFAULT_MIN_CONCURRENCY = 1;
|
|
6
|
+
const DEFAULT_INITIAL_CONCURRENCY = 4;
|
|
7
|
+
const DEFAULT_SMOOTHING_FACTOR = 0.3;
|
|
8
|
+
const DEFAULT_PROBE_INTERVAL = 8;
|
|
9
|
+
/**
|
|
10
|
+
* Gradient-based concurrency controller.
|
|
11
|
+
*
|
|
12
|
+
* Measures throughput via an exponential moving average (EMA) and adjusts the
|
|
13
|
+
* concurrency level to maximise task completion speed. The controller probes
|
|
14
|
+
* every {@link SwayOptions.probeInterval | probeInterval} completed tasks,
|
|
15
|
+
* compares the current EMA throughput against the previous one, and nudges
|
|
16
|
+
* concurrency up or down by one depending on the gradient direction.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const controller = new AdaptiveController({ maxConcurrency: 16 });
|
|
21
|
+
* controller.getConcurrency(); // 4 (default initial)
|
|
22
|
+
* controller.recordCompletion();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
class AdaptiveController {
|
|
26
|
+
/**
|
|
27
|
+
* @param options - Tuning knobs for the controller (all optional)
|
|
28
|
+
*/
|
|
29
|
+
constructor(options) {
|
|
30
|
+
var _a, _b, _c, _d, _e;
|
|
31
|
+
this.completionsSinceLastProbe = 0;
|
|
32
|
+
this.emaThroughput = null;
|
|
33
|
+
this.previousEmaThroughput = null;
|
|
34
|
+
this.concurrencySum = 0;
|
|
35
|
+
this.concurrencySamples = 0;
|
|
36
|
+
this.adjustmentCount = 0;
|
|
37
|
+
this.maxConcurrency = (_a = options === null || options === void 0 ? void 0 : options.maxConcurrency) !== null && _a !== void 0 ? _a : DEFAULT_MAX_CONCURRENCY;
|
|
38
|
+
this.minConcurrency = (_b = options === null || options === void 0 ? void 0 : options.minConcurrency) !== null && _b !== void 0 ? _b : DEFAULT_MIN_CONCURRENCY;
|
|
39
|
+
this.concurrency =
|
|
40
|
+
(_c = options === null || options === void 0 ? void 0 : options.initialConcurrency) !== null && _c !== void 0 ? _c : DEFAULT_INITIAL_CONCURRENCY;
|
|
41
|
+
this.smoothingFactor = (_d = options === null || options === void 0 ? void 0 : options.smoothingFactor) !== null && _d !== void 0 ? _d : DEFAULT_SMOOTHING_FACTOR;
|
|
42
|
+
this.probeInterval = (_e = options === null || options === void 0 ? void 0 : options.probeInterval) !== null && _e !== void 0 ? _e : DEFAULT_PROBE_INTERVAL;
|
|
43
|
+
this.concurrency = this.clamp(this.concurrency);
|
|
44
|
+
this.peakConcurrency = this.concurrency;
|
|
45
|
+
this.lastProbeTime = performance.now();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Signal that a task has completed. Triggers a probe when
|
|
49
|
+
* {@link SwayOptions.probeInterval | probeInterval} completions have accumulated.
|
|
50
|
+
*/
|
|
51
|
+
recordCompletion() {
|
|
52
|
+
this.completionsSinceLastProbe++;
|
|
53
|
+
this.concurrencySum += this.concurrency;
|
|
54
|
+
this.concurrencySamples++;
|
|
55
|
+
if (this.completionsSinceLastProbe >= this.probeInterval) {
|
|
56
|
+
this.probe();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Returns the current concurrency level. */
|
|
60
|
+
getConcurrency() {
|
|
61
|
+
return this.concurrency;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build a {@link SwayStats} snapshot.
|
|
65
|
+
*
|
|
66
|
+
* @param totalTasks - Total tasks executed
|
|
67
|
+
* @param totalDurationMs - Wall-clock duration of the run in milliseconds
|
|
68
|
+
* @returns Performance telemetry
|
|
69
|
+
*/
|
|
70
|
+
getStats(totalTasks, totalDurationMs) {
|
|
71
|
+
return {
|
|
72
|
+
totalTasks,
|
|
73
|
+
totalDurationMs,
|
|
74
|
+
peakConcurrency: this.peakConcurrency,
|
|
75
|
+
avgConcurrency: this.concurrencySamples > 0
|
|
76
|
+
? this.concurrencySum / this.concurrencySamples
|
|
77
|
+
: this.concurrency,
|
|
78
|
+
adjustments: this.adjustmentCount,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
probe() {
|
|
82
|
+
const now = performance.now();
|
|
83
|
+
const elapsed = now - this.lastProbeTime;
|
|
84
|
+
if (elapsed <= 0) {
|
|
85
|
+
this.completionsSinceLastProbe = 0;
|
|
86
|
+
this.lastProbeTime = now;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const currentThroughput = this.completionsSinceLastProbe / (elapsed / 1000);
|
|
90
|
+
if (this.emaThroughput === null) {
|
|
91
|
+
this.emaThroughput = currentThroughput;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
this.emaThroughput =
|
|
95
|
+
this.smoothingFactor * currentThroughput +
|
|
96
|
+
(1 - this.smoothingFactor) * this.emaThroughput;
|
|
97
|
+
}
|
|
98
|
+
if (this.previousEmaThroughput !== null) {
|
|
99
|
+
const gradient = this.emaThroughput - this.previousEmaThroughput;
|
|
100
|
+
if (gradient > 0) {
|
|
101
|
+
this.setConcurrency(this.concurrency + 1);
|
|
102
|
+
}
|
|
103
|
+
else if (gradient < 0) {
|
|
104
|
+
this.setConcurrency(this.concurrency - 1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
this.previousEmaThroughput = this.emaThroughput;
|
|
108
|
+
this.completionsSinceLastProbe = 0;
|
|
109
|
+
this.lastProbeTime = now;
|
|
110
|
+
}
|
|
111
|
+
setConcurrency(value) {
|
|
112
|
+
const clamped = this.clamp(value);
|
|
113
|
+
if (clamped !== this.concurrency) {
|
|
114
|
+
this.concurrency = clamped;
|
|
115
|
+
this.adjustmentCount++;
|
|
116
|
+
if (this.concurrency > this.peakConcurrency) {
|
|
117
|
+
this.peakConcurrency = this.concurrency;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
clamp(value) {
|
|
122
|
+
return Math.max(this.minConcurrency, Math.min(this.maxConcurrency, value));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
exports.AdaptiveController = AdaptiveController;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AdaptiveController = exports.sway = void 0;
|
|
4
|
+
var sway_js_1 = require("./sway.js");
|
|
5
|
+
Object.defineProperty(exports, "sway", { enumerable: true, get: function () { return sway_js_1.sway; } });
|
|
6
|
+
var adaptive_controller_js_1 = require("./adaptive-controller.js");
|
|
7
|
+
Object.defineProperty(exports, "AdaptiveController", { enumerable: true, get: function () { return adaptive_controller_js_1.AdaptiveController; } });
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for {@link sway}.
|
|
3
|
+
*/
|
|
4
|
+
export interface SwayOptions {
|
|
5
|
+
/** Upper bound for concurrent in-flight tasks (default: `64`) */
|
|
6
|
+
maxConcurrency?: number;
|
|
7
|
+
/** Lower bound for concurrent in-flight tasks (default: `1`) */
|
|
8
|
+
minConcurrency?: number;
|
|
9
|
+
/** Number of concurrent in-flight tasks to start with (default: `4`) */
|
|
10
|
+
initialConcurrency?: number;
|
|
11
|
+
/** EMA smoothing factor, ratio between 0 and 1 — lower values produce calmer adjustments (default: `0.3`) */
|
|
12
|
+
smoothingFactor?: number;
|
|
13
|
+
/** Number of completed tasks between probe adjustments (default: `8`) */
|
|
14
|
+
probeInterval?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Performance telemetry collected during a {@link sway} run.
|
|
18
|
+
*/
|
|
19
|
+
export interface SwayStats {
|
|
20
|
+
/** Total number of tasks that were executed */
|
|
21
|
+
totalTasks: number;
|
|
22
|
+
/** Wall-clock duration of the entire run in milliseconds */
|
|
23
|
+
totalDurationMs: number;
|
|
24
|
+
/** Highest concurrency level reached */
|
|
25
|
+
peakConcurrency: number;
|
|
26
|
+
/** Weighted average concurrency level across the run */
|
|
27
|
+
avgConcurrency: number;
|
|
28
|
+
/** Number of times the controller changed the concurrency level */
|
|
29
|
+
adjustments: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Return value of {@link sway}.
|
|
33
|
+
*
|
|
34
|
+
* @typeParam T - The resolved type of each task
|
|
35
|
+
*/
|
|
36
|
+
export interface SwayResult<T> {
|
|
37
|
+
/** Resolved values in the same order as the input tasks */
|
|
38
|
+
results: T[];
|
|
39
|
+
/** Performance telemetry for the run */
|
|
40
|
+
stats: SwayStats;
|
|
41
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SwayOptions, SwayResult } from './interfaces.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run async tasks concurrently with adaptive concurrency control.
|
|
4
|
+
*
|
|
5
|
+
* Works like `Promise.all()` but automatically tunes the number of in-flight
|
|
6
|
+
* tasks using a gradient-based controller that maximises throughput.
|
|
7
|
+
* Rejects on the first error (fail-fast), just like `Promise.all()`.
|
|
8
|
+
*
|
|
9
|
+
* @typeParam T - The resolved type of each task
|
|
10
|
+
* @param tasks - An iterable of zero-argument async functions (thunks)
|
|
11
|
+
* @param options - Optional tuning parameters for the adaptive controller
|
|
12
|
+
* @returns Resolved values in input order together with performance stats
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { sway } from '@icazemier/sway';
|
|
17
|
+
*
|
|
18
|
+
* const { results, stats } = await sway(
|
|
19
|
+
* urls.map(url => () => fetch(url).then(r => r.json())),
|
|
20
|
+
* { maxConcurrency: 16 }
|
|
21
|
+
* );
|
|
22
|
+
* console.log(stats.peakConcurrency, stats.avgConcurrency);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function sway<T>(tasks: Iterable<() => Promise<T>>, options?: SwayOptions): Promise<SwayResult<T>>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sway = sway;
|
|
4
|
+
const adaptive_controller_js_1 = require("./adaptive-controller.js");
|
|
5
|
+
/**
|
|
6
|
+
* Run async tasks concurrently with adaptive concurrency control.
|
|
7
|
+
*
|
|
8
|
+
* Works like `Promise.all()` but automatically tunes the number of in-flight
|
|
9
|
+
* tasks using a gradient-based controller that maximises throughput.
|
|
10
|
+
* Rejects on the first error (fail-fast), just like `Promise.all()`.
|
|
11
|
+
*
|
|
12
|
+
* @typeParam T - The resolved type of each task
|
|
13
|
+
* @param tasks - An iterable of zero-argument async functions (thunks)
|
|
14
|
+
* @param options - Optional tuning parameters for the adaptive controller
|
|
15
|
+
* @returns Resolved values in input order together with performance stats
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { sway } from '@icazemier/sway';
|
|
20
|
+
*
|
|
21
|
+
* const { results, stats } = await sway(
|
|
22
|
+
* urls.map(url => () => fetch(url).then(r => r.json())),
|
|
23
|
+
* { maxConcurrency: 16 }
|
|
24
|
+
* );
|
|
25
|
+
* console.log(stats.peakConcurrency, stats.avgConcurrency);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
async function sway(tasks, options) {
|
|
29
|
+
const controller = new adaptive_controller_js_1.AdaptiveController(options);
|
|
30
|
+
const iterator = tasks[Symbol.iterator]();
|
|
31
|
+
const results = [];
|
|
32
|
+
const startTime = performance.now();
|
|
33
|
+
let nextIndex = 0;
|
|
34
|
+
let activeTasks = 0;
|
|
35
|
+
let settled = false;
|
|
36
|
+
let totalTasks = 0;
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const tryReject = (error) => {
|
|
39
|
+
if (!settled) {
|
|
40
|
+
settled = true;
|
|
41
|
+
reject(error);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const scheduleNext = () => {
|
|
45
|
+
while (!settled && activeTasks < controller.getConcurrency()) {
|
|
46
|
+
const next = iterator.next();
|
|
47
|
+
if (next.done) {
|
|
48
|
+
if (activeTasks === 0) {
|
|
49
|
+
settled = true;
|
|
50
|
+
const totalDurationMs = performance.now() - startTime;
|
|
51
|
+
resolve({
|
|
52
|
+
results,
|
|
53
|
+
stats: controller.getStats(totalTasks, totalDurationMs),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const index = nextIndex++;
|
|
59
|
+
totalTasks++;
|
|
60
|
+
activeTasks++;
|
|
61
|
+
next
|
|
62
|
+
.value()
|
|
63
|
+
.then((value) => {
|
|
64
|
+
if (settled)
|
|
65
|
+
return;
|
|
66
|
+
results[index] = value;
|
|
67
|
+
activeTasks--;
|
|
68
|
+
controller.recordCompletion();
|
|
69
|
+
scheduleNext();
|
|
70
|
+
})
|
|
71
|
+
.catch(tryReject);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
scheduleNext();
|
|
75
|
+
if (nextIndex === 0 && activeTasks === 0) {
|
|
76
|
+
settled = true;
|
|
77
|
+
const totalDurationMs = performance.now() - startTime;
|
|
78
|
+
resolve({
|
|
79
|
+
results,
|
|
80
|
+
stats: controller.getStats(0, totalDurationMs),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { SwayOptions, SwayStats } from './interfaces.js';
|
|
2
|
+
/**
|
|
3
|
+
* Gradient-based concurrency controller.
|
|
4
|
+
*
|
|
5
|
+
* Measures throughput via an exponential moving average (EMA) and adjusts the
|
|
6
|
+
* concurrency level to maximise task completion speed. The controller probes
|
|
7
|
+
* every {@link SwayOptions.probeInterval | probeInterval} completed tasks,
|
|
8
|
+
* compares the current EMA throughput against the previous one, and nudges
|
|
9
|
+
* concurrency up or down by one depending on the gradient direction.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const controller = new AdaptiveController({ maxConcurrency: 16 });
|
|
14
|
+
* controller.getConcurrency(); // 4 (default initial)
|
|
15
|
+
* controller.recordCompletion();
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare class AdaptiveController {
|
|
19
|
+
private concurrency;
|
|
20
|
+
private readonly maxConcurrency;
|
|
21
|
+
private readonly minConcurrency;
|
|
22
|
+
private readonly smoothingFactor;
|
|
23
|
+
private readonly probeInterval;
|
|
24
|
+
private completionsSinceLastProbe;
|
|
25
|
+
private emaThroughput;
|
|
26
|
+
private previousEmaThroughput;
|
|
27
|
+
private lastProbeTime;
|
|
28
|
+
private peakConcurrency;
|
|
29
|
+
private concurrencySum;
|
|
30
|
+
private concurrencySamples;
|
|
31
|
+
private adjustmentCount;
|
|
32
|
+
/**
|
|
33
|
+
* @param options - Tuning knobs for the controller (all optional)
|
|
34
|
+
*/
|
|
35
|
+
constructor(options?: SwayOptions);
|
|
36
|
+
/**
|
|
37
|
+
* Signal that a task has completed. Triggers a probe when
|
|
38
|
+
* {@link SwayOptions.probeInterval | probeInterval} completions have accumulated.
|
|
39
|
+
*/
|
|
40
|
+
recordCompletion(): void;
|
|
41
|
+
/** Returns the current concurrency level. */
|
|
42
|
+
getConcurrency(): number;
|
|
43
|
+
/**
|
|
44
|
+
* Build a {@link SwayStats} snapshot.
|
|
45
|
+
*
|
|
46
|
+
* @param totalTasks - Total tasks executed
|
|
47
|
+
* @param totalDurationMs - Wall-clock duration of the run in milliseconds
|
|
48
|
+
* @returns Performance telemetry
|
|
49
|
+
*/
|
|
50
|
+
getStats(totalTasks: number, totalDurationMs: number): SwayStats;
|
|
51
|
+
private probe;
|
|
52
|
+
private setConcurrency;
|
|
53
|
+
private clamp;
|
|
54
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const DEFAULT_MAX_CONCURRENCY = 64;
|
|
2
|
+
const DEFAULT_MIN_CONCURRENCY = 1;
|
|
3
|
+
const DEFAULT_INITIAL_CONCURRENCY = 4;
|
|
4
|
+
const DEFAULT_SMOOTHING_FACTOR = 0.3;
|
|
5
|
+
const DEFAULT_PROBE_INTERVAL = 8;
|
|
6
|
+
/**
|
|
7
|
+
* Gradient-based concurrency controller.
|
|
8
|
+
*
|
|
9
|
+
* Measures throughput via an exponential moving average (EMA) and adjusts the
|
|
10
|
+
* concurrency level to maximise task completion speed. The controller probes
|
|
11
|
+
* every {@link SwayOptions.probeInterval | probeInterval} completed tasks,
|
|
12
|
+
* compares the current EMA throughput against the previous one, and nudges
|
|
13
|
+
* concurrency up or down by one depending on the gradient direction.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const controller = new AdaptiveController({ maxConcurrency: 16 });
|
|
18
|
+
* controller.getConcurrency(); // 4 (default initial)
|
|
19
|
+
* controller.recordCompletion();
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export class AdaptiveController {
|
|
23
|
+
concurrency;
|
|
24
|
+
maxConcurrency;
|
|
25
|
+
minConcurrency;
|
|
26
|
+
smoothingFactor;
|
|
27
|
+
probeInterval;
|
|
28
|
+
completionsSinceLastProbe = 0;
|
|
29
|
+
emaThroughput = null;
|
|
30
|
+
previousEmaThroughput = null;
|
|
31
|
+
lastProbeTime;
|
|
32
|
+
peakConcurrency;
|
|
33
|
+
concurrencySum = 0;
|
|
34
|
+
concurrencySamples = 0;
|
|
35
|
+
adjustmentCount = 0;
|
|
36
|
+
/**
|
|
37
|
+
* @param options - Tuning knobs for the controller (all optional)
|
|
38
|
+
*/
|
|
39
|
+
constructor(options) {
|
|
40
|
+
this.maxConcurrency = options?.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY;
|
|
41
|
+
this.minConcurrency = options?.minConcurrency ?? DEFAULT_MIN_CONCURRENCY;
|
|
42
|
+
this.concurrency =
|
|
43
|
+
options?.initialConcurrency ?? DEFAULT_INITIAL_CONCURRENCY;
|
|
44
|
+
this.smoothingFactor = options?.smoothingFactor ?? DEFAULT_SMOOTHING_FACTOR;
|
|
45
|
+
this.probeInterval = options?.probeInterval ?? DEFAULT_PROBE_INTERVAL;
|
|
46
|
+
this.concurrency = this.clamp(this.concurrency);
|
|
47
|
+
this.peakConcurrency = this.concurrency;
|
|
48
|
+
this.lastProbeTime = performance.now();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Signal that a task has completed. Triggers a probe when
|
|
52
|
+
* {@link SwayOptions.probeInterval | probeInterval} completions have accumulated.
|
|
53
|
+
*/
|
|
54
|
+
recordCompletion() {
|
|
55
|
+
this.completionsSinceLastProbe++;
|
|
56
|
+
this.concurrencySum += this.concurrency;
|
|
57
|
+
this.concurrencySamples++;
|
|
58
|
+
if (this.completionsSinceLastProbe >= this.probeInterval) {
|
|
59
|
+
this.probe();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Returns the current concurrency level. */
|
|
63
|
+
getConcurrency() {
|
|
64
|
+
return this.concurrency;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build a {@link SwayStats} snapshot.
|
|
68
|
+
*
|
|
69
|
+
* @param totalTasks - Total tasks executed
|
|
70
|
+
* @param totalDurationMs - Wall-clock duration of the run in milliseconds
|
|
71
|
+
* @returns Performance telemetry
|
|
72
|
+
*/
|
|
73
|
+
getStats(totalTasks, totalDurationMs) {
|
|
74
|
+
return {
|
|
75
|
+
totalTasks,
|
|
76
|
+
totalDurationMs,
|
|
77
|
+
peakConcurrency: this.peakConcurrency,
|
|
78
|
+
avgConcurrency: this.concurrencySamples > 0
|
|
79
|
+
? this.concurrencySum / this.concurrencySamples
|
|
80
|
+
: this.concurrency,
|
|
81
|
+
adjustments: this.adjustmentCount,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
probe() {
|
|
85
|
+
const now = performance.now();
|
|
86
|
+
const elapsed = now - this.lastProbeTime;
|
|
87
|
+
if (elapsed <= 0) {
|
|
88
|
+
this.completionsSinceLastProbe = 0;
|
|
89
|
+
this.lastProbeTime = now;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const currentThroughput = this.completionsSinceLastProbe / (elapsed / 1000);
|
|
93
|
+
if (this.emaThroughput === null) {
|
|
94
|
+
this.emaThroughput = currentThroughput;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.emaThroughput =
|
|
98
|
+
this.smoothingFactor * currentThroughput +
|
|
99
|
+
(1 - this.smoothingFactor) * this.emaThroughput;
|
|
100
|
+
}
|
|
101
|
+
if (this.previousEmaThroughput !== null) {
|
|
102
|
+
const gradient = this.emaThroughput - this.previousEmaThroughput;
|
|
103
|
+
if (gradient > 0) {
|
|
104
|
+
this.setConcurrency(this.concurrency + 1);
|
|
105
|
+
}
|
|
106
|
+
else if (gradient < 0) {
|
|
107
|
+
this.setConcurrency(this.concurrency - 1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
this.previousEmaThroughput = this.emaThroughput;
|
|
111
|
+
this.completionsSinceLastProbe = 0;
|
|
112
|
+
this.lastProbeTime = now;
|
|
113
|
+
}
|
|
114
|
+
setConcurrency(value) {
|
|
115
|
+
const clamped = this.clamp(value);
|
|
116
|
+
if (clamped !== this.concurrency) {
|
|
117
|
+
this.concurrency = clamped;
|
|
118
|
+
this.adjustmentCount++;
|
|
119
|
+
if (this.concurrency > this.peakConcurrency) {
|
|
120
|
+
this.peakConcurrency = this.concurrency;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
clamp(value) {
|
|
125
|
+
return Math.max(this.minConcurrency, Math.min(this.maxConcurrency, value));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for {@link sway}.
|
|
3
|
+
*/
|
|
4
|
+
export interface SwayOptions {
|
|
5
|
+
/** Upper bound for concurrent in-flight tasks (default: `64`) */
|
|
6
|
+
maxConcurrency?: number;
|
|
7
|
+
/** Lower bound for concurrent in-flight tasks (default: `1`) */
|
|
8
|
+
minConcurrency?: number;
|
|
9
|
+
/** Number of concurrent in-flight tasks to start with (default: `4`) */
|
|
10
|
+
initialConcurrency?: number;
|
|
11
|
+
/** EMA smoothing factor, ratio between 0 and 1 — lower values produce calmer adjustments (default: `0.3`) */
|
|
12
|
+
smoothingFactor?: number;
|
|
13
|
+
/** Number of completed tasks between probe adjustments (default: `8`) */
|
|
14
|
+
probeInterval?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Performance telemetry collected during a {@link sway} run.
|
|
18
|
+
*/
|
|
19
|
+
export interface SwayStats {
|
|
20
|
+
/** Total number of tasks that were executed */
|
|
21
|
+
totalTasks: number;
|
|
22
|
+
/** Wall-clock duration of the entire run in milliseconds */
|
|
23
|
+
totalDurationMs: number;
|
|
24
|
+
/** Highest concurrency level reached */
|
|
25
|
+
peakConcurrency: number;
|
|
26
|
+
/** Weighted average concurrency level across the run */
|
|
27
|
+
avgConcurrency: number;
|
|
28
|
+
/** Number of times the controller changed the concurrency level */
|
|
29
|
+
adjustments: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Return value of {@link sway}.
|
|
33
|
+
*
|
|
34
|
+
* @typeParam T - The resolved type of each task
|
|
35
|
+
*/
|
|
36
|
+
export interface SwayResult<T> {
|
|
37
|
+
/** Resolved values in the same order as the input tasks */
|
|
38
|
+
results: T[];
|
|
39
|
+
/** Performance telemetry for the run */
|
|
40
|
+
stats: SwayStats;
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SwayOptions, SwayResult } from './interfaces.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run async tasks concurrently with adaptive concurrency control.
|
|
4
|
+
*
|
|
5
|
+
* Works like `Promise.all()` but automatically tunes the number of in-flight
|
|
6
|
+
* tasks using a gradient-based controller that maximises throughput.
|
|
7
|
+
* Rejects on the first error (fail-fast), just like `Promise.all()`.
|
|
8
|
+
*
|
|
9
|
+
* @typeParam T - The resolved type of each task
|
|
10
|
+
* @param tasks - An iterable of zero-argument async functions (thunks)
|
|
11
|
+
* @param options - Optional tuning parameters for the adaptive controller
|
|
12
|
+
* @returns Resolved values in input order together with performance stats
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { sway } from '@icazemier/sway';
|
|
17
|
+
*
|
|
18
|
+
* const { results, stats } = await sway(
|
|
19
|
+
* urls.map(url => () => fetch(url).then(r => r.json())),
|
|
20
|
+
* { maxConcurrency: 16 }
|
|
21
|
+
* );
|
|
22
|
+
* console.log(stats.peakConcurrency, stats.avgConcurrency);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function sway<T>(tasks: Iterable<() => Promise<T>>, options?: SwayOptions): Promise<SwayResult<T>>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AdaptiveController } from './adaptive-controller.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run async tasks concurrently with adaptive concurrency control.
|
|
4
|
+
*
|
|
5
|
+
* Works like `Promise.all()` but automatically tunes the number of in-flight
|
|
6
|
+
* tasks using a gradient-based controller that maximises throughput.
|
|
7
|
+
* Rejects on the first error (fail-fast), just like `Promise.all()`.
|
|
8
|
+
*
|
|
9
|
+
* @typeParam T - The resolved type of each task
|
|
10
|
+
* @param tasks - An iterable of zero-argument async functions (thunks)
|
|
11
|
+
* @param options - Optional tuning parameters for the adaptive controller
|
|
12
|
+
* @returns Resolved values in input order together with performance stats
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { sway } from '@icazemier/sway';
|
|
17
|
+
*
|
|
18
|
+
* const { results, stats } = await sway(
|
|
19
|
+
* urls.map(url => () => fetch(url).then(r => r.json())),
|
|
20
|
+
* { maxConcurrency: 16 }
|
|
21
|
+
* );
|
|
22
|
+
* console.log(stats.peakConcurrency, stats.avgConcurrency);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export async function sway(tasks, options) {
|
|
26
|
+
const controller = new AdaptiveController(options);
|
|
27
|
+
const iterator = tasks[Symbol.iterator]();
|
|
28
|
+
const results = [];
|
|
29
|
+
const startTime = performance.now();
|
|
30
|
+
let nextIndex = 0;
|
|
31
|
+
let activeTasks = 0;
|
|
32
|
+
let settled = false;
|
|
33
|
+
let totalTasks = 0;
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const tryReject = (error) => {
|
|
36
|
+
if (!settled) {
|
|
37
|
+
settled = true;
|
|
38
|
+
reject(error);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const scheduleNext = () => {
|
|
42
|
+
while (!settled && activeTasks < controller.getConcurrency()) {
|
|
43
|
+
const next = iterator.next();
|
|
44
|
+
if (next.done) {
|
|
45
|
+
if (activeTasks === 0) {
|
|
46
|
+
settled = true;
|
|
47
|
+
const totalDurationMs = performance.now() - startTime;
|
|
48
|
+
resolve({
|
|
49
|
+
results,
|
|
50
|
+
stats: controller.getStats(totalTasks, totalDurationMs),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const index = nextIndex++;
|
|
56
|
+
totalTasks++;
|
|
57
|
+
activeTasks++;
|
|
58
|
+
next
|
|
59
|
+
.value()
|
|
60
|
+
.then((value) => {
|
|
61
|
+
if (settled)
|
|
62
|
+
return;
|
|
63
|
+
results[index] = value;
|
|
64
|
+
activeTasks--;
|
|
65
|
+
controller.recordCompletion();
|
|
66
|
+
scheduleNext();
|
|
67
|
+
})
|
|
68
|
+
.catch(tryReject);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
scheduleNext();
|
|
72
|
+
if (nextIndex === 0 && activeTasks === 0) {
|
|
73
|
+
settled = true;
|
|
74
|
+
const totalDurationMs = performance.now() - startTime;
|
|
75
|
+
resolve({
|
|
76
|
+
results,
|
|
77
|
+
stats: controller.getStats(0, totalDurationMs),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@icazemier/sway",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "1.0.0-beta.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Adaptive concurrent task runner — like Promise.all() with gradient-based concurrency control",
|
|
7
|
+
"contributors": [
|
|
8
|
+
{
|
|
9
|
+
"name": "Ivo Cazemier",
|
|
10
|
+
"email": "git@warmemelk.nl",
|
|
11
|
+
"url": "https://github.com/icazemier"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"concurrency",
|
|
16
|
+
"async",
|
|
17
|
+
"promise",
|
|
18
|
+
"parallel",
|
|
19
|
+
"adaptive",
|
|
20
|
+
"throttle",
|
|
21
|
+
"task runner"
|
|
22
|
+
],
|
|
23
|
+
"files": [
|
|
24
|
+
"build/"
|
|
25
|
+
],
|
|
26
|
+
"main": "./build/cjs/index.js",
|
|
27
|
+
"module": "./build/esm/index.js",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"import": "./build/esm/index.js",
|
|
31
|
+
"require": "./build/cjs/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"types": "./build/esm/index.d.ts",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20.0.0"
|
|
37
|
+
},
|
|
38
|
+
"os": [
|
|
39
|
+
"darwin",
|
|
40
|
+
"linux",
|
|
41
|
+
"win32"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "cross-env NODE_ENV=test vitest run --coverage",
|
|
45
|
+
"build:esm": "tsc -p tsconfig-build-esm.json",
|
|
46
|
+
"build:cjs": "tsc -p tsconfig-build-cjs.json",
|
|
47
|
+
"build": "rimraf build && npm run build:esm && npm run build:cjs && npm run fixup && npm run docs",
|
|
48
|
+
"docs": "typedoc --out docs src/index.ts",
|
|
49
|
+
"fixup": "node ./fixup.mjs",
|
|
50
|
+
"clean:build": "rimraf build",
|
|
51
|
+
"clean:docs": "rimraf docs",
|
|
52
|
+
"lint": "eslint . --ext .ts",
|
|
53
|
+
"lint:fix": "eslint . --ext .ts --fix",
|
|
54
|
+
"prepare": "husky",
|
|
55
|
+
"release": "semantic-release",
|
|
56
|
+
"release:dry": "semantic-release --dry-run"
|
|
57
|
+
},
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "https://github.com/icazemier/sway"
|
|
61
|
+
},
|
|
62
|
+
"homepage": "https://github.com/icazemier/sway#readme",
|
|
63
|
+
"bugs": {
|
|
64
|
+
"url": "https://github.com/icazemier/sway/issues"
|
|
65
|
+
},
|
|
66
|
+
"license": "MIT",
|
|
67
|
+
"publishConfig": {
|
|
68
|
+
"access": "public"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@commitlint/cli": "^19.6.0",
|
|
72
|
+
"@commitlint/config-conventional": "^19.6.0",
|
|
73
|
+
"@eslint/js": "^9.26.0",
|
|
74
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
75
|
+
"@semantic-release/git": "^10.0.1",
|
|
76
|
+
"@types/node": "^22.15.15",
|
|
77
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
78
|
+
"conventional-changelog-conventionalcommits": "^8.0.0",
|
|
79
|
+
"cross-env": "^7.0.3",
|
|
80
|
+
"eslint": "^9.26.0",
|
|
81
|
+
"eslint-config-prettier": "^10.1.3",
|
|
82
|
+
"eslint-plugin-prettier": "^5.4.0",
|
|
83
|
+
"husky": "^9.1.7",
|
|
84
|
+
"rimraf": "^6.0.1",
|
|
85
|
+
"semantic-release": "^25.0.3",
|
|
86
|
+
"typedoc": "^0.28.3",
|
|
87
|
+
"typescript": "^5.2.2",
|
|
88
|
+
"typescript-eslint": "^8.31.1",
|
|
89
|
+
"vitest": "^4.0.18"
|
|
90
|
+
}
|
|
91
|
+
}
|