@fuzdev/fuz_util 0.50.1 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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/path.ts CHANGED
@@ -37,7 +37,10 @@ export const to_file_path = (path_or_url: string | URL): string =>
37
37
  typeof path_or_url === 'string' ? path_or_url : decodeURIComponent(path_or_url.pathname);
38
38
 
39
39
  /**
40
- * @example parse_path_parts('./foo/bar/baz.ts') => ['foo', 'foo/bar', 'foo/bar/baz.ts']
40
+ * @example
41
+ * ```ts
42
+ * parse_path_parts('./foo/bar/baz.ts') // => ['foo', 'foo/bar', 'foo/bar/baz.ts']
43
+ * ```
41
44
  */
42
45
  export const parse_path_parts = (path: string): Array<string> => {
43
46
  const segments = parse_path_segments(path);
@@ -53,7 +56,10 @@ export const parse_path_parts = (path: string): Array<string> => {
53
56
 
54
57
  /**
55
58
  * Gets the individual parts of a path, ignoring dots and separators.
56
- * @example parse_path_segments('/foo/bar/baz.ts') => ['foo', 'bar', 'baz.ts']
59
+ * @example
60
+ * ```ts
61
+ * parse_path_segments('/foo/bar/baz.ts') // => ['foo', 'bar', 'baz.ts']
62
+ * ```
57
63
  */
58
64
  export const parse_path_segments = (path: string): Array<string> =>
59
65
  path.split('/').filter((s) => s && s !== '.' && s !== '..');
@@ -616,7 +616,10 @@ export const spawn_detached = (
616
616
  /**
617
617
  * Formats a child process for display.
618
618
  *
619
- * @example `pid(1234) <- node server.js`
619
+ * @example
620
+ * ```ts
621
+ * `pid(1234) <- node server.js`
622
+ * ```
620
623
  */
621
624
  export const print_child_process = (child: ChildProcess): string =>
622
625
  `${st('gray', 'pid(')}${child.pid ?? 'none'}${st('gray', ')')} ← ${st('green', child.spawnargs.join(' '))}`;
@@ -72,6 +72,16 @@ export const ComponentPropInfo = z.looseObject({
72
72
  description: z.string().optional(),
73
73
  default_value: z.string().optional(),
74
74
  bindable: z.boolean().optional(),
75
+ /** Code examples from `@example` tags. */
76
+ examples: z.array(z.string()).optional(),
77
+ /** Deprecation message from `@deprecated` tag. */
78
+ deprecated_message: z.string().optional(),
79
+ /** Related items from `@see` tags, in raw TSDoc format. */
80
+ see_also: z.array(z.string()).optional(),
81
+ /** Exceptions from `@throws` tags. */
82
+ throws: z.array(z.looseObject({type: z.string().optional(), description: z.string()})).optional(),
83
+ /** Version introduced, from `@since` tag. */
84
+ since: z.string().optional(),
75
85
  });
76
86
  export type ComponentPropInfo = z.infer<typeof ComponentPropInfo>;
77
87
 
@@ -110,6 +120,8 @@ export const DeclarationJson = z.looseObject({
110
120
  throws: z.array(z.looseObject({type: z.string().optional(), description: z.string()})).optional(),
111
121
  /** Version introduced, from `@since` tag. */
112
122
  since: z.string().optional(),
123
+ /** Mutation documentation from `@mutates` tags (non-standard), mapping parameter names to descriptions. */
124
+ mutates: z.record(z.string(), z.string()).optional(),
113
125
  /** Extended classes/interfaces. */
114
126
  extends: z.array(z.string()).optional(),
115
127
  /** Implemented interfaces. */
@@ -136,6 +148,7 @@ export const DeclarationJson = z.looseObject({
136
148
  .object({
137
149
  module: z.string(),
138
150
  name: z.string(),
151
+ kind: DeclarationKind,
139
152
  })
140
153
  .optional(),
141
154
  });
@@ -174,8 +187,14 @@ export type SourceJson = z.infer<typeof SourceJson>;
174
187
 
175
188
  /**
176
189
  * Format declaration name with generic parameters for display.
177
- * @example declaration_get_display_name({name: 'Map', kind: 'type', generic_params: [{name: 'K'}, {name: 'V'}]})
190
+ *
191
+ * @deprecated Use `getDisplayName` from `@fuzdev/svelte-docinfo/types.js` instead.
192
+ *
193
+ * @example
194
+ * ```ts
195
+ * declaration_get_display_name({name: 'Map', kind: 'type', generic_params: [{name: 'K'}, {name: 'V'}]})
178
196
  * // => 'Map<K, V>'
197
+ * ```
179
198
  */
180
199
  export const declaration_get_display_name = (declaration: DeclarationJson): string => {
181
200
  if (!declaration.generic_params?.length) return declaration.name;
@@ -190,8 +209,14 @@ export const declaration_get_display_name = (declaration: DeclarationJson): stri
190
209
 
191
210
  /**
192
211
  * Generate TypeScript import statement for a declaration.
193
- * @example declaration_generate_import({name: 'Foo', kind: 'type'}, 'foo.ts', '@pkg/lib')
212
+ *
213
+ * @deprecated Use `generateImport` from `@fuzdev/svelte-docinfo/types.js` instead.
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * declaration_generate_import({name: 'Foo', kind: 'type'}, 'foo.ts', '@pkg/lib')
194
218
  * // => "import type {Foo} from '@pkg/lib/foo.js';"
219
+ * ```
195
220
  */
196
221
  export const declaration_generate_import = (
197
222
  declaration: DeclarationJson,