@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 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
- * Creates a deferred object with a promise and its resolve/reject handlers.
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 a object with a `promise` and its `resolve`/`reject` handlers.
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 an async function on each item with controlled concurrency.
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 array of items to process
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: Array<T>, fn: (item: T, index: number) => Promise<void>, concurrency: number) => Promise<void>;
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 array of items to process
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: Array<T>, fn: (item: T, index: number) => Promise<R>, concurrency: number) => Promise<Array<R>>;
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
- * @param items array of items to process
63
- * @param fn async function to apply to each item
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, fetch, 5);
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: Array<T>, fn: (item: T, index: number) => Promise<R>, concurrency: number) => Promise<Array<PromiseSettledResult<R>>>;
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
  *
@@ -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;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,eAAe,GAAU,CAAC,EACtC,OAAO,KAAK,CAAC,CAAC,CAAC,EACf,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,EAC7C,aAAa,MAAM,KACjB,OAAO,CAAC,IAAI,CAgDd,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,cAAc,GAAU,CAAC,EAAE,CAAC,EACxC,OAAO,KAAK,CAAC,CAAC,CAAC,EACf,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,EAC1C,aAAa,MAAM,KACjB,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAkDlB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,sBAAsB,GAAU,CAAC,EAAE,CAAC,EAChD,OAAO,KAAK,CAAC,CAAC,CAAC,EACf,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,EAC1C,aAAa,MAAM,KACjB,OAAO,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CA6CxC,CAAC;AAEF;;;;GAIG;AACH,qBAAa,cAAc;;gBAId,OAAO,EAAE,MAAM;IAIrB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAU9B,OAAO,IAAI,IAAI;CAQf"}
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 a object with a `promise` and its `resolve`/`reject` handlers.
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 an async function on each item with controlled concurrency.
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 array of items to process
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, concurrency) => {
39
- if (concurrency < 1) {
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 run_next = () => {
47
- // Stop spawning if we've rejected
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
- // Check if we're done
51
- if (next_index >= items.length && active_count === 0) {
52
- resolve();
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 && next_index < items.length) {
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 = items[index];
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((error) => {
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 array of items to process
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, concurrency) => {
101
- if (concurrency < 1) {
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 = new Array(items.length);
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 run_next = () => {
110
- // Stop spawning if we've rejected
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
- // Check if we're done
114
- if (next_index >= items.length && active_count === 0) {
115
- resolve(results);
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 && next_index < items.length) {
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 = items[index];
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((error) => {
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
- * @param items array of items to process
152
- * @param fn async function to apply to each item
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, fetch, 5);
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, concurrency) => {
169
- if (concurrency < 1) {
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 = new Array(items.length);
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 run_next = () => {
177
- // Check if we're done
178
- if (next_index >= items.length && active_count === 0) {
179
- resolve(results);
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 && next_index < items.length) {
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 = items[index];
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
- results[index] = { status: 'fulfilled', value };
246
+ if (!aborted)
247
+ results[index] = { status: 'fulfilled', value };
190
248
  })
191
249
  .catch((reason) => {
192
- results[index] = { status: 'rejected', reason };
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
- async acquire() {
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.51.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.0",
73
- "@fuzdev/fuz_css": "^0.50.0",
74
- "@fuzdev/fuz_ui": "^0.183.1",
75
- "@fuzdev/gro": "^0.192.1",
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
- * Creates a deferred object with a promise and its resolve/reject handlers.
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 a object with a `promise` and its `resolve`/`reject` handlers.
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 an async function on each item with controlled concurrency.
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 array of items to process
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: Array<T>,
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 < 1) {
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 run_next = (): void => {
69
- // Stop spawning if we've rejected
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
- // Check if we're done
73
- if (next_index >= items.length && active_count === 0) {
74
- resolve();
75
- return;
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 && next_index < items.length) {
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 = items[index]!;
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((error) => {
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 array of items to process
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: Array<T>,
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 < 1) {
147
+ if (!(concurrency >= 1)) {
131
148
  throw new Error('concurrency must be at least 1');
132
149
  }
133
150
 
134
- const results: Array<R> = new Array(items.length);
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 run_next = (): void => {
141
- // Stop spawning if we've rejected
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
- // Check if we're done
145
- if (next_index >= items.length && active_count === 0) {
146
- resolve(results);
147
- return;
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 && next_index < items.length) {
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 = items[index]!;
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((error) => {
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
- * @param items array of items to process
186
- * @param fn async function to apply to each item
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, fetch, 5);
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: Array<T>,
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 < 1) {
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>> = new Array(items.length);
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 run_next = (): void => {
217
- // Check if we're done
218
- if (next_index >= items.length && active_count === 0) {
219
- resolve(results);
220
- return;
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 && next_index < items.length) {
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 = items[index]!;
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
- async acquire(): Promise<void> {
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
+ };