@fuzdev/fuz_util 0.51.0 → 0.52.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/dist/async.d.ts +21 -15
- package/dist/async.d.ts.map +1 -1
- package/dist/async.js +134 -75
- package/dist/zod.d.ts +76 -0
- package/dist/zod.d.ts.map +1 -0
- package/dist/zod.js +198 -0
- package/package.json +5 -5
- package/src/lib/async.ts +146 -83
- package/src/lib/zod.ts +218 -0
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/dist/zod.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schema introspection utilities.
|
|
3
|
+
*
|
|
4
|
+
* Generic helpers for extracting metadata from Zod schemas.
|
|
5
|
+
* Designed for CLI argument parsing but applicable elsewhere.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type { z } from 'zod';
|
|
10
|
+
/**
|
|
11
|
+
* Unwrap nested schema types (optional, default, nullable, etc).
|
|
12
|
+
*
|
|
13
|
+
* @param def - Zod type definition to unwrap.
|
|
14
|
+
* @returns Inner schema if wrapped, undefined otherwise.
|
|
15
|
+
*/
|
|
16
|
+
export declare const zod_to_subschema: (def: z.core.$ZodTypeDef) => z.ZodType | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Get the description from a schema's metadata, unwrapping if needed.
|
|
19
|
+
*
|
|
20
|
+
* @param schema - Zod schema to extract description from.
|
|
21
|
+
* @returns Description string or null if not found.
|
|
22
|
+
*/
|
|
23
|
+
export declare const zod_to_schema_description: (schema: z.ZodType) => string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Get the default value from a schema, unwrapping if needed.
|
|
26
|
+
*
|
|
27
|
+
* @param schema - Zod schema to extract default from.
|
|
28
|
+
* @returns Default value or undefined.
|
|
29
|
+
*/
|
|
30
|
+
export declare const zod_to_schema_default: (schema: z.ZodType) => unknown;
|
|
31
|
+
/**
|
|
32
|
+
* Get aliases from a schema's metadata, unwrapping if needed.
|
|
33
|
+
*
|
|
34
|
+
* @param schema - Zod schema to extract aliases from.
|
|
35
|
+
* @returns Array of alias strings.
|
|
36
|
+
*/
|
|
37
|
+
export declare const zod_to_schema_aliases: (schema: z.ZodType) => Array<string>;
|
|
38
|
+
/**
|
|
39
|
+
* Get the type string for a schema, suitable for display.
|
|
40
|
+
*
|
|
41
|
+
* @param schema - Zod schema to get type string for.
|
|
42
|
+
* @returns Human-readable type string.
|
|
43
|
+
*/
|
|
44
|
+
export declare const zod_to_schema_type_string: (schema: z.ZodType) => string;
|
|
45
|
+
/**
|
|
46
|
+
* Format a value for display in help text.
|
|
47
|
+
*
|
|
48
|
+
* @param value - Value to format.
|
|
49
|
+
* @returns Formatted string representation.
|
|
50
|
+
*/
|
|
51
|
+
export declare const zod_format_value: (value: unknown) => string;
|
|
52
|
+
/**
|
|
53
|
+
* Property extracted from an object schema.
|
|
54
|
+
*/
|
|
55
|
+
export interface ZodSchemaProperty {
|
|
56
|
+
name: string;
|
|
57
|
+
type: string;
|
|
58
|
+
description: string;
|
|
59
|
+
default: unknown;
|
|
60
|
+
aliases: Array<string>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Extract properties from a Zod object schema.
|
|
64
|
+
*
|
|
65
|
+
* @param schema - Zod object schema to extract from.
|
|
66
|
+
* @returns Array of property definitions.
|
|
67
|
+
*/
|
|
68
|
+
export declare const zod_to_schema_properties: (schema: z.ZodType) => Array<ZodSchemaProperty>;
|
|
69
|
+
/**
|
|
70
|
+
* Get all property names and their aliases from an object schema.
|
|
71
|
+
*
|
|
72
|
+
* @param schema - Zod object schema.
|
|
73
|
+
* @returns Set of all names and aliases.
|
|
74
|
+
*/
|
|
75
|
+
export declare const zod_to_schema_names_with_aliases: (schema: z.ZodType) => Set<string>;
|
|
76
|
+
//# sourceMappingURL=zod.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/zod.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAM3B;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,KAAK,CAAC,CAAC,IAAI,CAAC,WAAW,KAAG,CAAC,CAAC,OAAO,GAAG,SAStE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,MAAM,GAAG,IAUtE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAUzD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,KAAK,CAAC,MAAM,CAUrE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,MA4C7D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,OAAO,KAAG,MAQjD,CAAC;AAMF;;GAEG;AACH,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACvB;AAED;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,KAAK,CAAC,iBAAiB,CAuBnF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,GAAG,CAAC,MAAM,CAW9E,CAAC"}
|
package/dist/zod.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schema introspection utilities.
|
|
3
|
+
*
|
|
4
|
+
* Generic helpers for extracting metadata from Zod schemas.
|
|
5
|
+
* Designed for CLI argument parsing but applicable elsewhere.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
//
|
|
10
|
+
// Schema Introspection
|
|
11
|
+
//
|
|
12
|
+
/**
|
|
13
|
+
* Unwrap nested schema types (optional, default, nullable, etc).
|
|
14
|
+
*
|
|
15
|
+
* @param def - Zod type definition to unwrap.
|
|
16
|
+
* @returns Inner schema if wrapped, undefined otherwise.
|
|
17
|
+
*/
|
|
18
|
+
export const zod_to_subschema = (def) => {
|
|
19
|
+
if ('innerType' in def) {
|
|
20
|
+
return def.innerType;
|
|
21
|
+
}
|
|
22
|
+
else if ('in' in def) {
|
|
23
|
+
return def.in;
|
|
24
|
+
}
|
|
25
|
+
else if ('schema' in def) {
|
|
26
|
+
return def.schema;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Get the description from a schema's metadata, unwrapping if needed.
|
|
32
|
+
*
|
|
33
|
+
* @param schema - Zod schema to extract description from.
|
|
34
|
+
* @returns Description string or null if not found.
|
|
35
|
+
*/
|
|
36
|
+
export const zod_to_schema_description = (schema) => {
|
|
37
|
+
const meta = schema.meta();
|
|
38
|
+
if (meta?.description) {
|
|
39
|
+
return meta.description;
|
|
40
|
+
}
|
|
41
|
+
const subschema = zod_to_subschema(schema.def);
|
|
42
|
+
if (subschema) {
|
|
43
|
+
return zod_to_schema_description(subschema);
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Get the default value from a schema, unwrapping if needed.
|
|
49
|
+
*
|
|
50
|
+
* @param schema - Zod schema to extract default from.
|
|
51
|
+
* @returns Default value or undefined.
|
|
52
|
+
*/
|
|
53
|
+
export const zod_to_schema_default = (schema) => {
|
|
54
|
+
const { def } = schema._zod;
|
|
55
|
+
if ('defaultValue' in def) {
|
|
56
|
+
return def.defaultValue;
|
|
57
|
+
}
|
|
58
|
+
const subschema = zod_to_subschema(def);
|
|
59
|
+
if (subschema) {
|
|
60
|
+
return zod_to_schema_default(subschema);
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Get aliases from a schema's metadata, unwrapping if needed.
|
|
66
|
+
*
|
|
67
|
+
* @param schema - Zod schema to extract aliases from.
|
|
68
|
+
* @returns Array of alias strings.
|
|
69
|
+
*/
|
|
70
|
+
export const zod_to_schema_aliases = (schema) => {
|
|
71
|
+
const meta = schema.meta();
|
|
72
|
+
if (meta?.aliases) {
|
|
73
|
+
return meta.aliases;
|
|
74
|
+
}
|
|
75
|
+
const subschema = zod_to_subschema(schema.def);
|
|
76
|
+
if (subschema) {
|
|
77
|
+
return zod_to_schema_aliases(subschema);
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Get the type string for a schema, suitable for display.
|
|
83
|
+
*
|
|
84
|
+
* @param schema - Zod schema to get type string for.
|
|
85
|
+
* @returns Human-readable type string.
|
|
86
|
+
*/
|
|
87
|
+
export const zod_to_schema_type_string = (schema) => {
|
|
88
|
+
const { def } = schema._zod;
|
|
89
|
+
switch (def.type) {
|
|
90
|
+
case 'string':
|
|
91
|
+
return 'string';
|
|
92
|
+
case 'number':
|
|
93
|
+
return 'number';
|
|
94
|
+
case 'int':
|
|
95
|
+
return 'int';
|
|
96
|
+
case 'boolean':
|
|
97
|
+
return 'boolean';
|
|
98
|
+
case 'bigint':
|
|
99
|
+
return 'bigint';
|
|
100
|
+
case 'null':
|
|
101
|
+
return 'null';
|
|
102
|
+
case 'undefined':
|
|
103
|
+
return 'undefined';
|
|
104
|
+
case 'any':
|
|
105
|
+
return 'any';
|
|
106
|
+
case 'unknown':
|
|
107
|
+
return 'unknown';
|
|
108
|
+
case 'array':
|
|
109
|
+
return 'Array<string>';
|
|
110
|
+
case 'enum':
|
|
111
|
+
return schema.options
|
|
112
|
+
.map((v) => `'${v}'`)
|
|
113
|
+
.join(' | ');
|
|
114
|
+
case 'literal':
|
|
115
|
+
return def.values
|
|
116
|
+
.map((v) => zod_format_value(v))
|
|
117
|
+
.join(' | ');
|
|
118
|
+
case 'nullable': {
|
|
119
|
+
const subschema = zod_to_subschema(def);
|
|
120
|
+
return subschema ? zod_to_schema_type_string(subschema) + ' | null' : 'nullable';
|
|
121
|
+
}
|
|
122
|
+
case 'optional': {
|
|
123
|
+
const subschema = zod_to_subschema(def);
|
|
124
|
+
return subschema ? zod_to_schema_type_string(subschema) + ' | undefined' : 'optional';
|
|
125
|
+
}
|
|
126
|
+
default: {
|
|
127
|
+
const subschema = zod_to_subschema(def);
|
|
128
|
+
return subschema ? zod_to_schema_type_string(subschema) : def.type;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Format a value for display in help text.
|
|
134
|
+
*
|
|
135
|
+
* @param value - Value to format.
|
|
136
|
+
* @returns Formatted string representation.
|
|
137
|
+
*/
|
|
138
|
+
export const zod_format_value = (value) => {
|
|
139
|
+
if (value === undefined)
|
|
140
|
+
return '';
|
|
141
|
+
if (value === null)
|
|
142
|
+
return 'null';
|
|
143
|
+
if (typeof value === 'string')
|
|
144
|
+
return `'${value}'`;
|
|
145
|
+
if (Array.isArray(value))
|
|
146
|
+
return '[]';
|
|
147
|
+
if (typeof value === 'object')
|
|
148
|
+
return JSON.stringify(value);
|
|
149
|
+
if (typeof value === 'boolean' || typeof value === 'number')
|
|
150
|
+
return String(value);
|
|
151
|
+
return '';
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Extract properties from a Zod object schema.
|
|
155
|
+
*
|
|
156
|
+
* @param schema - Zod object schema to extract from.
|
|
157
|
+
* @returns Array of property definitions.
|
|
158
|
+
*/
|
|
159
|
+
export const zod_to_schema_properties = (schema) => {
|
|
160
|
+
const { def } = schema;
|
|
161
|
+
if (!('shape' in def)) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
const shape = def.shape;
|
|
165
|
+
const properties = [];
|
|
166
|
+
for (const name in shape) {
|
|
167
|
+
// Skip no- prefixed fields (used for boolean negation)
|
|
168
|
+
if ('no-' + name in shape)
|
|
169
|
+
continue;
|
|
170
|
+
const field = shape[name];
|
|
171
|
+
properties.push({
|
|
172
|
+
name,
|
|
173
|
+
type: zod_to_schema_type_string(field),
|
|
174
|
+
description: zod_to_schema_description(field) ?? '',
|
|
175
|
+
default: zod_to_schema_default(field),
|
|
176
|
+
aliases: zod_to_schema_aliases(field),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return properties;
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Get all property names and their aliases from an object schema.
|
|
183
|
+
*
|
|
184
|
+
* @param schema - Zod object schema.
|
|
185
|
+
* @returns Set of all names and aliases.
|
|
186
|
+
*/
|
|
187
|
+
export const zod_to_schema_names_with_aliases = (schema) => {
|
|
188
|
+
const names = new Set();
|
|
189
|
+
for (const prop of zod_to_schema_properties(schema)) {
|
|
190
|
+
if (prop.name !== '_') {
|
|
191
|
+
names.add(prop.name);
|
|
192
|
+
for (const alias of prop.aliases) {
|
|
193
|
+
names.add(alias);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return names;
|
|
198
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/fuz_util",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.1",
|
|
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.53.0",
|
|
74
|
+
"@fuzdev/fuz_ui": "^0.184.0",
|
|
75
|
+
"@fuzdev/gro": "^0.195.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);
|
package/src/lib/zod.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schema introspection utilities.
|
|
3
|
+
*
|
|
4
|
+
* Generic helpers for extracting metadata from Zod schemas.
|
|
5
|
+
* Designed for CLI argument parsing but applicable elsewhere.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {z} from 'zod';
|
|
11
|
+
|
|
12
|
+
//
|
|
13
|
+
// Schema Introspection
|
|
14
|
+
//
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Unwrap nested schema types (optional, default, nullable, etc).
|
|
18
|
+
*
|
|
19
|
+
* @param def - Zod type definition to unwrap.
|
|
20
|
+
* @returns Inner schema if wrapped, undefined otherwise.
|
|
21
|
+
*/
|
|
22
|
+
export const zod_to_subschema = (def: z.core.$ZodTypeDef): z.ZodType | undefined => {
|
|
23
|
+
if ('innerType' in def) {
|
|
24
|
+
return def.innerType as z.ZodType;
|
|
25
|
+
} else if ('in' in def) {
|
|
26
|
+
return def.in as z.ZodType;
|
|
27
|
+
} else if ('schema' in def) {
|
|
28
|
+
return def.schema as z.ZodType;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the description from a schema's metadata, unwrapping if needed.
|
|
35
|
+
*
|
|
36
|
+
* @param schema - Zod schema to extract description from.
|
|
37
|
+
* @returns Description string or null if not found.
|
|
38
|
+
*/
|
|
39
|
+
export const zod_to_schema_description = (schema: z.ZodType): string | null => {
|
|
40
|
+
const meta = schema.meta();
|
|
41
|
+
if (meta?.description) {
|
|
42
|
+
return meta.description;
|
|
43
|
+
}
|
|
44
|
+
const subschema = zod_to_subschema(schema.def);
|
|
45
|
+
if (subschema) {
|
|
46
|
+
return zod_to_schema_description(subschema);
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the default value from a schema, unwrapping if needed.
|
|
53
|
+
*
|
|
54
|
+
* @param schema - Zod schema to extract default from.
|
|
55
|
+
* @returns Default value or undefined.
|
|
56
|
+
*/
|
|
57
|
+
export const zod_to_schema_default = (schema: z.ZodType): unknown => {
|
|
58
|
+
const {def} = schema._zod;
|
|
59
|
+
if ('defaultValue' in def) {
|
|
60
|
+
return def.defaultValue;
|
|
61
|
+
}
|
|
62
|
+
const subschema = zod_to_subschema(def);
|
|
63
|
+
if (subschema) {
|
|
64
|
+
return zod_to_schema_default(subschema);
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get aliases from a schema's metadata, unwrapping if needed.
|
|
71
|
+
*
|
|
72
|
+
* @param schema - Zod schema to extract aliases from.
|
|
73
|
+
* @returns Array of alias strings.
|
|
74
|
+
*/
|
|
75
|
+
export const zod_to_schema_aliases = (schema: z.ZodType): Array<string> => {
|
|
76
|
+
const meta = schema.meta();
|
|
77
|
+
if (meta?.aliases) {
|
|
78
|
+
return meta.aliases as Array<string>;
|
|
79
|
+
}
|
|
80
|
+
const subschema = zod_to_subschema(schema.def);
|
|
81
|
+
if (subschema) {
|
|
82
|
+
return zod_to_schema_aliases(subschema);
|
|
83
|
+
}
|
|
84
|
+
return [];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the type string for a schema, suitable for display.
|
|
89
|
+
*
|
|
90
|
+
* @param schema - Zod schema to get type string for.
|
|
91
|
+
* @returns Human-readable type string.
|
|
92
|
+
*/
|
|
93
|
+
export const zod_to_schema_type_string = (schema: z.ZodType): string => {
|
|
94
|
+
const {def} = schema._zod;
|
|
95
|
+
switch (def.type) {
|
|
96
|
+
case 'string':
|
|
97
|
+
return 'string';
|
|
98
|
+
case 'number':
|
|
99
|
+
return 'number';
|
|
100
|
+
case 'int':
|
|
101
|
+
return 'int';
|
|
102
|
+
case 'boolean':
|
|
103
|
+
return 'boolean';
|
|
104
|
+
case 'bigint':
|
|
105
|
+
return 'bigint';
|
|
106
|
+
case 'null':
|
|
107
|
+
return 'null';
|
|
108
|
+
case 'undefined':
|
|
109
|
+
return 'undefined';
|
|
110
|
+
case 'any':
|
|
111
|
+
return 'any';
|
|
112
|
+
case 'unknown':
|
|
113
|
+
return 'unknown';
|
|
114
|
+
case 'array':
|
|
115
|
+
return 'Array<string>';
|
|
116
|
+
case 'enum':
|
|
117
|
+
return (schema as unknown as {options: Array<string>}).options
|
|
118
|
+
.map((v) => `'${v}'`)
|
|
119
|
+
.join(' | ');
|
|
120
|
+
case 'literal':
|
|
121
|
+
return (def as unknown as {values: Array<unknown>}).values
|
|
122
|
+
.map((v) => zod_format_value(v))
|
|
123
|
+
.join(' | ');
|
|
124
|
+
case 'nullable': {
|
|
125
|
+
const subschema = zod_to_subschema(def);
|
|
126
|
+
return subschema ? zod_to_schema_type_string(subschema) + ' | null' : 'nullable';
|
|
127
|
+
}
|
|
128
|
+
case 'optional': {
|
|
129
|
+
const subschema = zod_to_subschema(def);
|
|
130
|
+
return subschema ? zod_to_schema_type_string(subschema) + ' | undefined' : 'optional';
|
|
131
|
+
}
|
|
132
|
+
default: {
|
|
133
|
+
const subschema = zod_to_subschema(def);
|
|
134
|
+
return subschema ? zod_to_schema_type_string(subschema) : def.type;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Format a value for display in help text.
|
|
141
|
+
*
|
|
142
|
+
* @param value - Value to format.
|
|
143
|
+
* @returns Formatted string representation.
|
|
144
|
+
*/
|
|
145
|
+
export const zod_format_value = (value: unknown): string => {
|
|
146
|
+
if (value === undefined) return '';
|
|
147
|
+
if (value === null) return 'null';
|
|
148
|
+
if (typeof value === 'string') return `'${value}'`;
|
|
149
|
+
if (Array.isArray(value)) return '[]';
|
|
150
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
151
|
+
if (typeof value === 'boolean' || typeof value === 'number') return String(value);
|
|
152
|
+
return '';
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
//
|
|
156
|
+
// Object Schema Helpers
|
|
157
|
+
//
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Property extracted from an object schema.
|
|
161
|
+
*/
|
|
162
|
+
export interface ZodSchemaProperty {
|
|
163
|
+
name: string;
|
|
164
|
+
type: string;
|
|
165
|
+
description: string;
|
|
166
|
+
default: unknown;
|
|
167
|
+
aliases: Array<string>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Extract properties from a Zod object schema.
|
|
172
|
+
*
|
|
173
|
+
* @param schema - Zod object schema to extract from.
|
|
174
|
+
* @returns Array of property definitions.
|
|
175
|
+
*/
|
|
176
|
+
export const zod_to_schema_properties = (schema: z.ZodType): Array<ZodSchemaProperty> => {
|
|
177
|
+
const {def} = schema;
|
|
178
|
+
|
|
179
|
+
if (!('shape' in def)) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
const shape = (def as z.core.$ZodObjectDef).shape;
|
|
183
|
+
|
|
184
|
+
const properties: Array<ZodSchemaProperty> = [];
|
|
185
|
+
for (const name in shape) {
|
|
186
|
+
// Skip no- prefixed fields (used for boolean negation)
|
|
187
|
+
if ('no-' + name in shape) continue;
|
|
188
|
+
|
|
189
|
+
const field = shape[name] as z.ZodType;
|
|
190
|
+
properties.push({
|
|
191
|
+
name,
|
|
192
|
+
type: zod_to_schema_type_string(field),
|
|
193
|
+
description: zod_to_schema_description(field) ?? '',
|
|
194
|
+
default: zod_to_schema_default(field),
|
|
195
|
+
aliases: zod_to_schema_aliases(field),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return properties;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get all property names and their aliases from an object schema.
|
|
203
|
+
*
|
|
204
|
+
* @param schema - Zod object schema.
|
|
205
|
+
* @returns Set of all names and aliases.
|
|
206
|
+
*/
|
|
207
|
+
export const zod_to_schema_names_with_aliases = (schema: z.ZodType): Set<string> => {
|
|
208
|
+
const names: Set<string> = new Set();
|
|
209
|
+
for (const prop of zod_to_schema_properties(schema)) {
|
|
210
|
+
if (prop.name !== '_') {
|
|
211
|
+
names.add(prop.name);
|
|
212
|
+
for (const alias of prop.aliases) {
|
|
213
|
+
names.add(alias);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return names;
|
|
218
|
+
};
|