@fuzdev/fuz_util 0.51.0 → 0.52.0
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/dist/async.d.ts +21 -15
- package/dist/async.d.ts.map +1 -1
- package/dist/async.js +134 -75
- package/package.json +5 -5
- package/src/lib/async.ts +146 -83
package/dist/async.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare const wait: (duration?: number) => Promise<void>;
|
|
|
8
8
|
*/
|
|
9
9
|
export declare const is_promise: (value: unknown) => value is Promise<unknown>;
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* A deferred object with a promise and its resolve/reject handlers.
|
|
12
12
|
*/
|
|
13
13
|
export interface Deferred<T> {
|
|
14
14
|
promise: Promise<T>;
|
|
@@ -16,57 +16,63 @@ export interface Deferred<T> {
|
|
|
16
16
|
reject: (reason: any) => void;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* Creates
|
|
19
|
+
* Creates an object with a `promise` and its `resolve`/`reject` handlers.
|
|
20
20
|
*/
|
|
21
21
|
export declare const create_deferred: <T>() => Deferred<T>;
|
|
22
22
|
/**
|
|
23
|
-
* Runs
|
|
23
|
+
* Runs a function on each item with controlled concurrency.
|
|
24
24
|
* Like `map_concurrent` but doesn't collect results (more efficient for side effects).
|
|
25
25
|
*
|
|
26
|
-
* @param items
|
|
27
|
-
* @param fn async function to apply to each item
|
|
26
|
+
* @param items items to process
|
|
28
27
|
* @param concurrency maximum number of concurrent operations
|
|
28
|
+
* @param fn function to apply to each item
|
|
29
|
+
* @param signal optional `AbortSignal` to cancel processing
|
|
29
30
|
*
|
|
30
31
|
* @example
|
|
31
32
|
* ```ts
|
|
32
33
|
* await each_concurrent(
|
|
33
34
|
* file_paths,
|
|
34
|
-
* async (path) => { await unlink(path); },
|
|
35
35
|
* 5, // max 5 concurrent deletions
|
|
36
|
+
* async (path) => { await unlink(path); },
|
|
36
37
|
* );
|
|
37
38
|
* ```
|
|
38
39
|
*/
|
|
39
|
-
export declare const each_concurrent: <T>(items:
|
|
40
|
+
export declare const each_concurrent: <T>(items: Iterable<T>, concurrency: number, fn: (item: T, index: number) => Promise<void> | void, signal?: AbortSignal) => Promise<void>;
|
|
40
41
|
/**
|
|
41
42
|
* Maps over items with controlled concurrency, preserving input order.
|
|
42
43
|
*
|
|
43
|
-
* @param items
|
|
44
|
-
* @param fn async function to apply to each item
|
|
44
|
+
* @param items items to process
|
|
45
45
|
* @param concurrency maximum number of concurrent operations
|
|
46
|
+
* @param fn function to apply to each item
|
|
47
|
+
* @param signal optional `AbortSignal` to cancel processing
|
|
46
48
|
* @returns promise resolving to array of results in same order as input
|
|
47
49
|
*
|
|
48
50
|
* @example
|
|
49
51
|
* ```ts
|
|
50
52
|
* const results = await map_concurrent(
|
|
51
53
|
* file_paths,
|
|
52
|
-
* async (path) => readFile(path, 'utf8'),
|
|
53
54
|
* 5, // max 5 concurrent reads
|
|
55
|
+
* async (path) => readFile(path, 'utf8'),
|
|
54
56
|
* );
|
|
55
57
|
* ```
|
|
56
58
|
*/
|
|
57
|
-
export declare const map_concurrent: <T, R>(items:
|
|
59
|
+
export declare const map_concurrent: <T, R>(items: Iterable<T>, concurrency: number, fn: (item: T, index: number) => Promise<R> | R, signal?: AbortSignal) => Promise<Array<R>>;
|
|
58
60
|
/**
|
|
59
61
|
* Like `map_concurrent` but collects all results/errors instead of failing fast.
|
|
60
62
|
* Returns an array of settlement objects matching the `Promise.allSettled` pattern.
|
|
61
63
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
+
* On abort, resolves with partial results: completed items keep their real settlements,
|
|
65
|
+
* in-flight and un-started items are settled as rejected with the abort reason.
|
|
66
|
+
*
|
|
67
|
+
* @param items items to process
|
|
64
68
|
* @param concurrency maximum number of concurrent operations
|
|
69
|
+
* @param fn function to apply to each item
|
|
70
|
+
* @param signal optional `AbortSignal` to cancel processing
|
|
65
71
|
* @returns promise resolving to array of `PromiseSettledResult` objects in input order
|
|
66
72
|
*
|
|
67
73
|
* @example
|
|
68
74
|
* ```ts
|
|
69
|
-
* const results = await map_concurrent_settled(urls,
|
|
75
|
+
* const results = await map_concurrent_settled(urls, 5, fetch);
|
|
70
76
|
* for (const [i, result] of results.entries()) {
|
|
71
77
|
* if (result.status === 'fulfilled') {
|
|
72
78
|
* console.log(`${urls[i]}: ${result.value.status}`);
|
|
@@ -76,7 +82,7 @@ export declare const map_concurrent: <T, R>(items: Array<T>, fn: (item: T, index
|
|
|
76
82
|
* }
|
|
77
83
|
* ```
|
|
78
84
|
*/
|
|
79
|
-
export declare const map_concurrent_settled: <T, R>(items:
|
|
85
|
+
export declare const map_concurrent_settled: <T, R>(items: Iterable<T>, concurrency: number, fn: (item: T, index: number) => Promise<R> | R, signal?: AbortSignal) => Promise<Array<PromiseSettledResult<R>>>;
|
|
80
86
|
/**
|
|
81
87
|
* Async semaphore for concurrency limiting.
|
|
82
88
|
*
|
package/dist/async.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/async.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAExE;;GAEG;AACH,eAAO,MAAM,IAAI,GAAI,iBAAY,KAAG,OAAO,CAAC,IAAI,CACQ,CAAC;AAEzD;;GAEG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,OAAO,CAAC,OAAO,CACI,CAAC;AAEzE;;GAEG;AACH,MAAM,WAAW,QAAQ,CAAC,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,MAAM,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CAC9B;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,OAAK,QAAQ,CAAC,CAAC,CAQ/C,CAAC;AAEF
|
|
1
|
+
{"version":3,"file":"async.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/async.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAExE;;GAEG;AACH,eAAO,MAAM,IAAI,GAAI,iBAAY,KAAG,OAAO,CAAC,IAAI,CACQ,CAAC;AAEzD;;GAEG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,OAAO,CAAC,OAAO,CACI,CAAC;AAEzE;;GAEG;AACH,MAAM,WAAW,QAAQ,CAAC,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,MAAM,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CAC9B;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,OAAK,QAAQ,CAAC,CAAC,CAQ/C,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,eAAe,GAAU,CAAC,EACtC,OAAO,QAAQ,CAAC,CAAC,CAAC,EAClB,aAAa,MAAM,EACnB,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,EACpD,SAAS,WAAW,KAClB,OAAO,CAAC,IAAI,CA6Dd,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,GAAU,CAAC,EAAE,CAAC,EACxC,OAAO,QAAQ,CAAC,CAAC,CAAC,EAClB,aAAa,MAAM,EACnB,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAC9C,SAAS,WAAW,KAClB,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CA+DlB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,sBAAsB,GAAU,CAAC,EAAE,CAAC,EAChD,OAAO,QAAQ,CAAC,CAAC,CAAC,EAClB,aAAa,MAAM,EACnB,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAC9C,SAAS,WAAW,KAClB,OAAO,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAsExC,CAAC;AAEF;;;;GAIG;AACH,qBAAa,cAAc;;gBAId,OAAO,EAAE,MAAM;IAO3B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAUxB,OAAO,IAAI,IAAI;CAQf"}
|
package/dist/async.js
CHANGED
|
@@ -7,7 +7,7 @@ export const wait = (duration = 0) => new Promise((resolve) => setTimeout(resolv
|
|
|
7
7
|
*/
|
|
8
8
|
export const is_promise = (value) => value != null && typeof value.then === 'function';
|
|
9
9
|
/**
|
|
10
|
-
* Creates
|
|
10
|
+
* Creates an object with a `promise` and its `resolve`/`reject` handlers.
|
|
11
11
|
*/
|
|
12
12
|
export const create_deferred = () => {
|
|
13
13
|
let resolve;
|
|
@@ -19,108 +19,142 @@ export const create_deferred = () => {
|
|
|
19
19
|
return { promise, resolve, reject };
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
|
-
* Runs
|
|
22
|
+
* Runs a function on each item with controlled concurrency.
|
|
23
23
|
* Like `map_concurrent` but doesn't collect results (more efficient for side effects).
|
|
24
24
|
*
|
|
25
|
-
* @param items
|
|
26
|
-
* @param fn async function to apply to each item
|
|
25
|
+
* @param items items to process
|
|
27
26
|
* @param concurrency maximum number of concurrent operations
|
|
27
|
+
* @param fn function to apply to each item
|
|
28
|
+
* @param signal optional `AbortSignal` to cancel processing
|
|
28
29
|
*
|
|
29
30
|
* @example
|
|
30
31
|
* ```ts
|
|
31
32
|
* await each_concurrent(
|
|
32
33
|
* file_paths,
|
|
33
|
-
* async (path) => { await unlink(path); },
|
|
34
34
|
* 5, // max 5 concurrent deletions
|
|
35
|
+
* async (path) => { await unlink(path); },
|
|
35
36
|
* );
|
|
36
37
|
* ```
|
|
37
38
|
*/
|
|
38
|
-
export const each_concurrent = async (items, fn,
|
|
39
|
-
if (concurrency
|
|
39
|
+
export const each_concurrent = async (items, concurrency, fn, signal) => {
|
|
40
|
+
if (!(concurrency >= 1)) {
|
|
40
41
|
throw new Error('concurrency must be at least 1');
|
|
41
42
|
}
|
|
43
|
+
const iterator = items[Symbol.iterator]();
|
|
42
44
|
let next_index = 0;
|
|
43
45
|
let active_count = 0;
|
|
44
46
|
let rejected = false;
|
|
45
47
|
return new Promise((resolve, reject) => {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
+
const cleanup = signal ? () => signal.removeEventListener('abort', on_abort) : undefined;
|
|
49
|
+
const done = () => {
|
|
50
|
+
cleanup?.();
|
|
51
|
+
resolve();
|
|
52
|
+
};
|
|
53
|
+
const fail = (error) => {
|
|
48
54
|
if (rejected)
|
|
49
55
|
return;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
rejected = true;
|
|
57
|
+
cleanup?.();
|
|
58
|
+
reject(error); // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors
|
|
59
|
+
};
|
|
60
|
+
function on_abort() {
|
|
61
|
+
fail(signal.reason);
|
|
62
|
+
}
|
|
63
|
+
if (signal?.aborted) {
|
|
64
|
+
fail(signal.reason);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
signal?.addEventListener('abort', on_abort);
|
|
68
|
+
const run_next = () => {
|
|
69
|
+
if (rejected)
|
|
53
70
|
return;
|
|
54
|
-
}
|
|
55
71
|
// Spawn workers up to concurrency limit
|
|
56
|
-
while (active_count < concurrency
|
|
72
|
+
while (active_count < concurrency) {
|
|
73
|
+
const next = iterator.next();
|
|
74
|
+
if (next.done) {
|
|
75
|
+
if (active_count === 0)
|
|
76
|
+
done();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
57
79
|
const index = next_index++;
|
|
58
|
-
const item =
|
|
80
|
+
const item = next.value;
|
|
59
81
|
active_count++;
|
|
60
|
-
fn(item, index)
|
|
82
|
+
new Promise((r) => r(fn(item, index)))
|
|
61
83
|
.then(() => {
|
|
62
84
|
if (rejected)
|
|
63
85
|
return;
|
|
64
86
|
active_count--;
|
|
65
87
|
run_next();
|
|
66
88
|
})
|
|
67
|
-
.catch(
|
|
68
|
-
if (rejected)
|
|
69
|
-
return;
|
|
70
|
-
rejected = true;
|
|
71
|
-
reject(error); // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors
|
|
72
|
-
});
|
|
89
|
+
.catch(fail);
|
|
73
90
|
}
|
|
74
91
|
};
|
|
75
|
-
// Handle empty array
|
|
76
|
-
if (items.length === 0) {
|
|
77
|
-
resolve();
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
92
|
run_next();
|
|
81
93
|
});
|
|
82
94
|
};
|
|
83
95
|
/**
|
|
84
96
|
* Maps over items with controlled concurrency, preserving input order.
|
|
85
97
|
*
|
|
86
|
-
* @param items
|
|
87
|
-
* @param fn async function to apply to each item
|
|
98
|
+
* @param items items to process
|
|
88
99
|
* @param concurrency maximum number of concurrent operations
|
|
100
|
+
* @param fn function to apply to each item
|
|
101
|
+
* @param signal optional `AbortSignal` to cancel processing
|
|
89
102
|
* @returns promise resolving to array of results in same order as input
|
|
90
103
|
*
|
|
91
104
|
* @example
|
|
92
105
|
* ```ts
|
|
93
106
|
* const results = await map_concurrent(
|
|
94
107
|
* file_paths,
|
|
95
|
-
* async (path) => readFile(path, 'utf8'),
|
|
96
108
|
* 5, // max 5 concurrent reads
|
|
109
|
+
* async (path) => readFile(path, 'utf8'),
|
|
97
110
|
* );
|
|
98
111
|
* ```
|
|
99
112
|
*/
|
|
100
|
-
export const map_concurrent = async (items, fn,
|
|
101
|
-
if (concurrency
|
|
113
|
+
export const map_concurrent = async (items, concurrency, fn, signal) => {
|
|
114
|
+
if (!(concurrency >= 1)) {
|
|
102
115
|
throw new Error('concurrency must be at least 1');
|
|
103
116
|
}
|
|
104
|
-
const results =
|
|
117
|
+
const results = [];
|
|
118
|
+
const iterator = items[Symbol.iterator]();
|
|
105
119
|
let next_index = 0;
|
|
106
120
|
let active_count = 0;
|
|
107
121
|
let rejected = false;
|
|
108
122
|
return new Promise((resolve, reject) => {
|
|
109
|
-
const
|
|
110
|
-
|
|
123
|
+
const cleanup = signal ? () => signal.removeEventListener('abort', on_abort) : undefined;
|
|
124
|
+
const done = () => {
|
|
125
|
+
cleanup?.();
|
|
126
|
+
resolve(results);
|
|
127
|
+
};
|
|
128
|
+
const fail = (error) => {
|
|
111
129
|
if (rejected)
|
|
112
130
|
return;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
131
|
+
rejected = true;
|
|
132
|
+
cleanup?.();
|
|
133
|
+
reject(error); // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors
|
|
134
|
+
};
|
|
135
|
+
function on_abort() {
|
|
136
|
+
fail(signal.reason);
|
|
137
|
+
}
|
|
138
|
+
if (signal?.aborted) {
|
|
139
|
+
fail(signal.reason);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
signal?.addEventListener('abort', on_abort);
|
|
143
|
+
const run_next = () => {
|
|
144
|
+
if (rejected)
|
|
116
145
|
return;
|
|
117
|
-
}
|
|
118
146
|
// Spawn workers up to concurrency limit
|
|
119
|
-
while (active_count < concurrency
|
|
147
|
+
while (active_count < concurrency) {
|
|
148
|
+
const next = iterator.next();
|
|
149
|
+
if (next.done) {
|
|
150
|
+
if (active_count === 0)
|
|
151
|
+
done();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
120
154
|
const index = next_index++;
|
|
121
|
-
const item =
|
|
155
|
+
const item = next.value;
|
|
122
156
|
active_count++;
|
|
123
|
-
fn(item, index)
|
|
157
|
+
new Promise((r) => r(fn(item, index)))
|
|
124
158
|
.then((result) => {
|
|
125
159
|
if (rejected)
|
|
126
160
|
return;
|
|
@@ -128,19 +162,9 @@ export const map_concurrent = async (items, fn, concurrency) => {
|
|
|
128
162
|
active_count--;
|
|
129
163
|
run_next();
|
|
130
164
|
})
|
|
131
|
-
.catch(
|
|
132
|
-
if (rejected)
|
|
133
|
-
return;
|
|
134
|
-
rejected = true;
|
|
135
|
-
reject(error); // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors
|
|
136
|
-
});
|
|
165
|
+
.catch(fail);
|
|
137
166
|
}
|
|
138
167
|
};
|
|
139
|
-
// Handle empty array
|
|
140
|
-
if (items.length === 0) {
|
|
141
|
-
resolve(results);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
168
|
run_next();
|
|
145
169
|
});
|
|
146
170
|
};
|
|
@@ -148,14 +172,18 @@ export const map_concurrent = async (items, fn, concurrency) => {
|
|
|
148
172
|
* Like `map_concurrent` but collects all results/errors instead of failing fast.
|
|
149
173
|
* Returns an array of settlement objects matching the `Promise.allSettled` pattern.
|
|
150
174
|
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
175
|
+
* On abort, resolves with partial results: completed items keep their real settlements,
|
|
176
|
+
* in-flight and un-started items are settled as rejected with the abort reason.
|
|
177
|
+
*
|
|
178
|
+
* @param items items to process
|
|
153
179
|
* @param concurrency maximum number of concurrent operations
|
|
180
|
+
* @param fn function to apply to each item
|
|
181
|
+
* @param signal optional `AbortSignal` to cancel processing
|
|
154
182
|
* @returns promise resolving to array of `PromiseSettledResult` objects in input order
|
|
155
183
|
*
|
|
156
184
|
* @example
|
|
157
185
|
* ```ts
|
|
158
|
-
* const results = await map_concurrent_settled(urls,
|
|
186
|
+
* const results = await map_concurrent_settled(urls, 5, fetch);
|
|
159
187
|
* for (const [i, result] of results.entries()) {
|
|
160
188
|
* if (result.status === 'fulfilled') {
|
|
161
189
|
* console.log(`${urls[i]}: ${result.value.status}`);
|
|
@@ -165,43 +193,71 @@ export const map_concurrent = async (items, fn, concurrency) => {
|
|
|
165
193
|
* }
|
|
166
194
|
* ```
|
|
167
195
|
*/
|
|
168
|
-
export const map_concurrent_settled = async (items, fn,
|
|
169
|
-
if (concurrency
|
|
196
|
+
export const map_concurrent_settled = async (items, concurrency, fn, signal) => {
|
|
197
|
+
if (!(concurrency >= 1)) {
|
|
170
198
|
throw new Error('concurrency must be at least 1');
|
|
171
199
|
}
|
|
172
|
-
const results =
|
|
200
|
+
const results = [];
|
|
201
|
+
const iterator = items[Symbol.iterator]();
|
|
173
202
|
let next_index = 0;
|
|
174
203
|
let active_count = 0;
|
|
204
|
+
let aborted = false;
|
|
175
205
|
return new Promise((resolve) => {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
206
|
+
const cleanup = signal ? () => signal.removeEventListener('abort', on_abort) : undefined;
|
|
207
|
+
const done = () => {
|
|
208
|
+
cleanup?.();
|
|
209
|
+
resolve(results);
|
|
210
|
+
};
|
|
211
|
+
function on_abort() {
|
|
212
|
+
if (aborted)
|
|
180
213
|
return;
|
|
214
|
+
aborted = true;
|
|
215
|
+
cleanup?.();
|
|
216
|
+
// Settle in-flight items as rejected with the abort reason
|
|
217
|
+
const reason = signal.reason;
|
|
218
|
+
for (let i = 0; i < next_index; i++) {
|
|
219
|
+
if (!(i in results)) {
|
|
220
|
+
results[i] = { status: 'rejected', reason };
|
|
221
|
+
}
|
|
181
222
|
}
|
|
223
|
+
resolve(results);
|
|
224
|
+
}
|
|
225
|
+
if (signal?.aborted) {
|
|
226
|
+
resolve(results);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
signal?.addEventListener('abort', on_abort);
|
|
230
|
+
const run_next = () => {
|
|
231
|
+
if (aborted)
|
|
232
|
+
return;
|
|
182
233
|
// Spawn workers up to concurrency limit
|
|
183
|
-
while (active_count < concurrency
|
|
234
|
+
while (active_count < concurrency) {
|
|
235
|
+
const next = iterator.next();
|
|
236
|
+
if (next.done) {
|
|
237
|
+
if (active_count === 0)
|
|
238
|
+
done();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
184
241
|
const index = next_index++;
|
|
185
|
-
const item =
|
|
242
|
+
const item = next.value;
|
|
186
243
|
active_count++;
|
|
187
|
-
fn(item, index)
|
|
244
|
+
new Promise((r) => r(fn(item, index)))
|
|
188
245
|
.then((value) => {
|
|
189
|
-
|
|
246
|
+
if (!aborted)
|
|
247
|
+
results[index] = { status: 'fulfilled', value };
|
|
190
248
|
})
|
|
191
249
|
.catch((reason) => {
|
|
192
|
-
|
|
250
|
+
if (!aborted)
|
|
251
|
+
results[index] = { status: 'rejected', reason };
|
|
193
252
|
})
|
|
194
253
|
.finally(() => {
|
|
254
|
+
if (aborted)
|
|
255
|
+
return;
|
|
195
256
|
active_count--;
|
|
196
257
|
run_next();
|
|
197
258
|
});
|
|
198
259
|
}
|
|
199
260
|
};
|
|
200
|
-
// Handle empty array
|
|
201
|
-
if (items.length === 0) {
|
|
202
|
-
resolve(results);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
261
|
run_next();
|
|
206
262
|
});
|
|
207
263
|
};
|
|
@@ -214,12 +270,15 @@ export class AsyncSemaphore {
|
|
|
214
270
|
#permits;
|
|
215
271
|
#waiters = [];
|
|
216
272
|
constructor(permits) {
|
|
273
|
+
if (!(permits >= 0)) {
|
|
274
|
+
throw new Error('permits must be >= 0');
|
|
275
|
+
}
|
|
217
276
|
this.#permits = permits;
|
|
218
277
|
}
|
|
219
|
-
|
|
278
|
+
acquire() {
|
|
220
279
|
if (this.#permits > 0) {
|
|
221
280
|
this.#permits--;
|
|
222
|
-
return;
|
|
281
|
+
return Promise.resolve();
|
|
223
282
|
}
|
|
224
283
|
return new Promise((resolve) => {
|
|
225
284
|
this.#waiters.push(resolve);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/fuz_util",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "utility belt for JS",
|
|
5
5
|
"glyph": "🦕",
|
|
6
6
|
"logo": "logo.svg",
|
|
@@ -69,10 +69,10 @@
|
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@changesets/changelog-git": "^0.2.1",
|
|
72
|
-
"@fuzdev/fuz_code": "^0.45.
|
|
73
|
-
"@fuzdev/fuz_css": "^0.
|
|
74
|
-
"@fuzdev/fuz_ui": "^0.
|
|
75
|
-
"@fuzdev/gro": "^0.
|
|
72
|
+
"@fuzdev/fuz_code": "^0.45.1",
|
|
73
|
+
"@fuzdev/fuz_css": "^0.52.0",
|
|
74
|
+
"@fuzdev/fuz_ui": "^0.184.0",
|
|
75
|
+
"@fuzdev/gro": "^0.194.0",
|
|
76
76
|
"@jridgewell/trace-mapping": "^0.3.31",
|
|
77
77
|
"@ryanatkn/eslint-config": "^0.9.0",
|
|
78
78
|
"@sveltejs/adapter-static": "^3.0.10",
|
package/src/lib/async.ts
CHANGED
|
@@ -13,7 +13,7 @@ export const is_promise = (value: unknown): value is Promise<unknown> =>
|
|
|
13
13
|
value != null && typeof (value as Promise<unknown>).then === 'function';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* A deferred object with a promise and its resolve/reject handlers.
|
|
17
17
|
*/
|
|
18
18
|
export interface Deferred<T> {
|
|
19
19
|
promise: Promise<T>;
|
|
@@ -22,7 +22,7 @@ export interface Deferred<T> {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Creates
|
|
25
|
+
* Creates an object with a `promise` and its `resolve`/`reject` handlers.
|
|
26
26
|
*/
|
|
27
27
|
export const create_deferred = <T>(): Deferred<T> => {
|
|
28
28
|
let resolve!: (value: T) => void;
|
|
@@ -35,72 +35,87 @@ export const create_deferred = <T>(): Deferred<T> => {
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Runs
|
|
38
|
+
* Runs a function on each item with controlled concurrency.
|
|
39
39
|
* Like `map_concurrent` but doesn't collect results (more efficient for side effects).
|
|
40
40
|
*
|
|
41
|
-
* @param items
|
|
42
|
-
* @param fn async function to apply to each item
|
|
41
|
+
* @param items items to process
|
|
43
42
|
* @param concurrency maximum number of concurrent operations
|
|
43
|
+
* @param fn function to apply to each item
|
|
44
|
+
* @param signal optional `AbortSignal` to cancel processing
|
|
44
45
|
*
|
|
45
46
|
* @example
|
|
46
47
|
* ```ts
|
|
47
48
|
* await each_concurrent(
|
|
48
49
|
* file_paths,
|
|
49
|
-
* async (path) => { await unlink(path); },
|
|
50
50
|
* 5, // max 5 concurrent deletions
|
|
51
|
+
* async (path) => { await unlink(path); },
|
|
51
52
|
* );
|
|
52
53
|
* ```
|
|
53
54
|
*/
|
|
54
55
|
export const each_concurrent = async <T>(
|
|
55
|
-
items:
|
|
56
|
-
fn: (item: T, index: number) => Promise<void>,
|
|
56
|
+
items: Iterable<T>,
|
|
57
57
|
concurrency: number,
|
|
58
|
+
fn: (item: T, index: number) => Promise<void> | void,
|
|
59
|
+
signal?: AbortSignal,
|
|
58
60
|
): Promise<void> => {
|
|
59
|
-
if (concurrency
|
|
61
|
+
if (!(concurrency >= 1)) {
|
|
60
62
|
throw new Error('concurrency must be at least 1');
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
const iterator = items[Symbol.iterator]();
|
|
63
66
|
let next_index = 0;
|
|
64
67
|
let active_count = 0;
|
|
65
68
|
let rejected = false;
|
|
66
69
|
|
|
67
70
|
return new Promise((resolve, reject) => {
|
|
68
|
-
const
|
|
69
|
-
|
|
71
|
+
const cleanup = signal ? () => signal.removeEventListener('abort', on_abort) : undefined;
|
|
72
|
+
|
|
73
|
+
const done = (): void => {
|
|
74
|
+
cleanup?.();
|
|
75
|
+
resolve();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const fail = (error: unknown): void => {
|
|
70
79
|
if (rejected) return;
|
|
80
|
+
rejected = true;
|
|
81
|
+
cleanup?.();
|
|
82
|
+
reject(error); // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors
|
|
83
|
+
};
|
|
71
84
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
function on_abort(): void {
|
|
86
|
+
fail(signal!.reason);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (signal?.aborted) {
|
|
90
|
+
fail(signal.reason);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
signal?.addEventListener('abort', on_abort);
|
|
94
|
+
|
|
95
|
+
const run_next = (): void => {
|
|
96
|
+
if (rejected) return;
|
|
77
97
|
|
|
78
98
|
// Spawn workers up to concurrency limit
|
|
79
|
-
while (active_count < concurrency
|
|
99
|
+
while (active_count < concurrency) {
|
|
100
|
+
const next = iterator.next();
|
|
101
|
+
if (next.done) {
|
|
102
|
+
if (active_count === 0) done();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
80
105
|
const index = next_index++;
|
|
81
|
-
const item =
|
|
106
|
+
const item = next.value;
|
|
82
107
|
active_count++;
|
|
83
108
|
|
|
84
|
-
fn(item, index)
|
|
109
|
+
new Promise<void>((r) => r(fn(item, index)))
|
|
85
110
|
.then(() => {
|
|
86
111
|
if (rejected) return;
|
|
87
112
|
active_count--;
|
|
88
113
|
run_next();
|
|
89
114
|
})
|
|
90
|
-
.catch(
|
|
91
|
-
if (rejected) return;
|
|
92
|
-
rejected = true;
|
|
93
|
-
reject(error); // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors
|
|
94
|
-
});
|
|
115
|
+
.catch(fail);
|
|
95
116
|
}
|
|
96
117
|
};
|
|
97
118
|
|
|
98
|
-
// Handle empty array
|
|
99
|
-
if (items.length === 0) {
|
|
100
|
-
resolve();
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
119
|
run_next();
|
|
105
120
|
});
|
|
106
121
|
};
|
|
@@ -108,72 +123,87 @@ export const each_concurrent = async <T>(
|
|
|
108
123
|
/**
|
|
109
124
|
* Maps over items with controlled concurrency, preserving input order.
|
|
110
125
|
*
|
|
111
|
-
* @param items
|
|
112
|
-
* @param fn async function to apply to each item
|
|
126
|
+
* @param items items to process
|
|
113
127
|
* @param concurrency maximum number of concurrent operations
|
|
128
|
+
* @param fn function to apply to each item
|
|
129
|
+
* @param signal optional `AbortSignal` to cancel processing
|
|
114
130
|
* @returns promise resolving to array of results in same order as input
|
|
115
131
|
*
|
|
116
132
|
* @example
|
|
117
133
|
* ```ts
|
|
118
134
|
* const results = await map_concurrent(
|
|
119
135
|
* file_paths,
|
|
120
|
-
* async (path) => readFile(path, 'utf8'),
|
|
121
136
|
* 5, // max 5 concurrent reads
|
|
137
|
+
* async (path) => readFile(path, 'utf8'),
|
|
122
138
|
* );
|
|
123
139
|
* ```
|
|
124
140
|
*/
|
|
125
141
|
export const map_concurrent = async <T, R>(
|
|
126
|
-
items:
|
|
127
|
-
fn: (item: T, index: number) => Promise<R>,
|
|
142
|
+
items: Iterable<T>,
|
|
128
143
|
concurrency: number,
|
|
144
|
+
fn: (item: T, index: number) => Promise<R> | R,
|
|
145
|
+
signal?: AbortSignal,
|
|
129
146
|
): Promise<Array<R>> => {
|
|
130
|
-
if (concurrency
|
|
147
|
+
if (!(concurrency >= 1)) {
|
|
131
148
|
throw new Error('concurrency must be at least 1');
|
|
132
149
|
}
|
|
133
150
|
|
|
134
|
-
const results: Array<R> =
|
|
151
|
+
const results: Array<R> = [];
|
|
152
|
+
const iterator = items[Symbol.iterator]();
|
|
135
153
|
let next_index = 0;
|
|
136
154
|
let active_count = 0;
|
|
137
155
|
let rejected = false;
|
|
138
156
|
|
|
139
157
|
return new Promise((resolve, reject) => {
|
|
140
|
-
const
|
|
141
|
-
|
|
158
|
+
const cleanup = signal ? () => signal.removeEventListener('abort', on_abort) : undefined;
|
|
159
|
+
|
|
160
|
+
const done = (): void => {
|
|
161
|
+
cleanup?.();
|
|
162
|
+
resolve(results);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const fail = (error: unknown): void => {
|
|
142
166
|
if (rejected) return;
|
|
167
|
+
rejected = true;
|
|
168
|
+
cleanup?.();
|
|
169
|
+
reject(error); // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors
|
|
170
|
+
};
|
|
143
171
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
172
|
+
function on_abort(): void {
|
|
173
|
+
fail(signal!.reason);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (signal?.aborted) {
|
|
177
|
+
fail(signal.reason);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
signal?.addEventListener('abort', on_abort);
|
|
181
|
+
|
|
182
|
+
const run_next = (): void => {
|
|
183
|
+
if (rejected) return;
|
|
149
184
|
|
|
150
185
|
// Spawn workers up to concurrency limit
|
|
151
|
-
while (active_count < concurrency
|
|
186
|
+
while (active_count < concurrency) {
|
|
187
|
+
const next = iterator.next();
|
|
188
|
+
if (next.done) {
|
|
189
|
+
if (active_count === 0) done();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
152
192
|
const index = next_index++;
|
|
153
|
-
const item =
|
|
193
|
+
const item = next.value;
|
|
154
194
|
active_count++;
|
|
155
195
|
|
|
156
|
-
fn(item, index)
|
|
196
|
+
new Promise<R>((r) => r(fn(item, index)))
|
|
157
197
|
.then((result) => {
|
|
158
198
|
if (rejected) return;
|
|
159
199
|
results[index] = result;
|
|
160
200
|
active_count--;
|
|
161
201
|
run_next();
|
|
162
202
|
})
|
|
163
|
-
.catch(
|
|
164
|
-
if (rejected) return;
|
|
165
|
-
rejected = true;
|
|
166
|
-
reject(error); // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors
|
|
167
|
-
});
|
|
203
|
+
.catch(fail);
|
|
168
204
|
}
|
|
169
205
|
};
|
|
170
206
|
|
|
171
|
-
// Handle empty array
|
|
172
|
-
if (items.length === 0) {
|
|
173
|
-
resolve(results);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
207
|
run_next();
|
|
178
208
|
});
|
|
179
209
|
};
|
|
@@ -182,14 +212,18 @@ export const map_concurrent = async <T, R>(
|
|
|
182
212
|
* Like `map_concurrent` but collects all results/errors instead of failing fast.
|
|
183
213
|
* Returns an array of settlement objects matching the `Promise.allSettled` pattern.
|
|
184
214
|
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
215
|
+
* On abort, resolves with partial results: completed items keep their real settlements,
|
|
216
|
+
* in-flight and un-started items are settled as rejected with the abort reason.
|
|
217
|
+
*
|
|
218
|
+
* @param items items to process
|
|
187
219
|
* @param concurrency maximum number of concurrent operations
|
|
220
|
+
* @param fn function to apply to each item
|
|
221
|
+
* @param signal optional `AbortSignal` to cancel processing
|
|
188
222
|
* @returns promise resolving to array of `PromiseSettledResult` objects in input order
|
|
189
223
|
*
|
|
190
224
|
* @example
|
|
191
225
|
* ```ts
|
|
192
|
-
* const results = await map_concurrent_settled(urls,
|
|
226
|
+
* const results = await map_concurrent_settled(urls, 5, fetch);
|
|
193
227
|
* for (const [i, result] of results.entries()) {
|
|
194
228
|
* if (result.status === 'fulfilled') {
|
|
195
229
|
* console.log(`${urls[i]}: ${result.value.status}`);
|
|
@@ -200,52 +234,78 @@ export const map_concurrent = async <T, R>(
|
|
|
200
234
|
* ```
|
|
201
235
|
*/
|
|
202
236
|
export const map_concurrent_settled = async <T, R>(
|
|
203
|
-
items:
|
|
204
|
-
fn: (item: T, index: number) => Promise<R>,
|
|
237
|
+
items: Iterable<T>,
|
|
205
238
|
concurrency: number,
|
|
239
|
+
fn: (item: T, index: number) => Promise<R> | R,
|
|
240
|
+
signal?: AbortSignal,
|
|
206
241
|
): Promise<Array<PromiseSettledResult<R>>> => {
|
|
207
|
-
if (concurrency
|
|
242
|
+
if (!(concurrency >= 1)) {
|
|
208
243
|
throw new Error('concurrency must be at least 1');
|
|
209
244
|
}
|
|
210
245
|
|
|
211
|
-
const results: Array<PromiseSettledResult<R>> =
|
|
246
|
+
const results: Array<PromiseSettledResult<R>> = [];
|
|
247
|
+
const iterator = items[Symbol.iterator]();
|
|
212
248
|
let next_index = 0;
|
|
213
249
|
let active_count = 0;
|
|
250
|
+
let aborted = false;
|
|
214
251
|
|
|
215
252
|
return new Promise((resolve) => {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
253
|
+
const cleanup = signal ? () => signal.removeEventListener('abort', on_abort) : undefined;
|
|
254
|
+
|
|
255
|
+
const done = (): void => {
|
|
256
|
+
cleanup?.();
|
|
257
|
+
resolve(results);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
function on_abort(): void {
|
|
261
|
+
if (aborted) return;
|
|
262
|
+
aborted = true;
|
|
263
|
+
cleanup?.();
|
|
264
|
+
// Settle in-flight items as rejected with the abort reason
|
|
265
|
+
const reason: unknown = signal!.reason;
|
|
266
|
+
for (let i = 0; i < next_index; i++) {
|
|
267
|
+
if (!(i in results)) {
|
|
268
|
+
results[i] = {status: 'rejected', reason};
|
|
269
|
+
}
|
|
221
270
|
}
|
|
271
|
+
resolve(results);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (signal?.aborted) {
|
|
275
|
+
resolve(results);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
signal?.addEventListener('abort', on_abort);
|
|
279
|
+
|
|
280
|
+
const run_next = (): void => {
|
|
281
|
+
if (aborted) return;
|
|
222
282
|
|
|
223
283
|
// Spawn workers up to concurrency limit
|
|
224
|
-
while (active_count < concurrency
|
|
284
|
+
while (active_count < concurrency) {
|
|
285
|
+
const next = iterator.next();
|
|
286
|
+
if (next.done) {
|
|
287
|
+
if (active_count === 0) done();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
225
290
|
const index = next_index++;
|
|
226
|
-
const item =
|
|
291
|
+
const item = next.value;
|
|
227
292
|
active_count++;
|
|
228
293
|
|
|
229
|
-
fn(item, index)
|
|
294
|
+
new Promise<R>((r) => r(fn(item, index)))
|
|
230
295
|
.then((value) => {
|
|
231
|
-
results[index] = {status: 'fulfilled', value};
|
|
296
|
+
if (!aborted) results[index] = {status: 'fulfilled', value};
|
|
232
297
|
})
|
|
233
298
|
.catch((reason: unknown) => {
|
|
234
|
-
results[index] = {status: 'rejected', reason};
|
|
299
|
+
if (!aborted) results[index] = {status: 'rejected', reason};
|
|
235
300
|
})
|
|
236
301
|
.finally(() => {
|
|
302
|
+
if (aborted) return;
|
|
237
303
|
active_count--;
|
|
238
304
|
run_next();
|
|
239
305
|
});
|
|
240
306
|
}
|
|
241
307
|
};
|
|
242
308
|
|
|
243
|
-
// Handle empty array
|
|
244
|
-
if (items.length === 0) {
|
|
245
|
-
resolve(results);
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
309
|
run_next();
|
|
250
310
|
});
|
|
251
311
|
};
|
|
@@ -260,13 +320,16 @@ export class AsyncSemaphore {
|
|
|
260
320
|
#waiters: Array<() => void> = [];
|
|
261
321
|
|
|
262
322
|
constructor(permits: number) {
|
|
323
|
+
if (!(permits >= 0)) {
|
|
324
|
+
throw new Error('permits must be >= 0');
|
|
325
|
+
}
|
|
263
326
|
this.#permits = permits;
|
|
264
327
|
}
|
|
265
328
|
|
|
266
|
-
|
|
329
|
+
acquire(): Promise<void> {
|
|
267
330
|
if (this.#permits > 0) {
|
|
268
331
|
this.#permits--;
|
|
269
|
-
return;
|
|
332
|
+
return Promise.resolve();
|
|
270
333
|
}
|
|
271
334
|
return new Promise<void>((resolve) => {
|
|
272
335
|
this.#waiters.push(resolve);
|