Skip to content

Commit 1638a63

Browse files
committed
Complete normalization of class jsdocs
1 parent d06daa4 commit 1638a63

File tree

3 files changed

+157
-118
lines changed

3 files changed

+157
-118
lines changed

src/iterable-mapper.ts

Lines changed: 74 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,41 @@ type NewElementOrError<NewElement = unknown> = {
8282
};
8383

8484
/**
85-
* Iterates over a source iterable with specified concurrency,
85+
* Iterates over a source iterable / generator with specified `concurrency`,
8686
* calling the `mapper` on each iterated item, and storing the
87-
* `mapper` result in a queue of specified max size, before
87+
* `mapper` result in a queue of `maxUnread` size, before
8888
* being iterated / read by the caller.
8989
*
9090
* @remarks
9191
*
92-
* Optimized for I/O-bound operations (e.g., fetching data, reading files) rather than
93-
* CPU-intensive tasks. The concurrent processing with backpressure ensures efficient
94-
* resource utilization without overwhelming memory or system resources.
92+
* ### Typical Use Case
93+
* - Prefetching items from an async I/O source
94+
* - In the simple sequential (`concurrency: 1`) case, allows items to be prefetched async, preserving order, while caller processes an item
95+
* - Can allow parallel prefetches for sources that allow for out of order reads (`concurrency: 2+`)
96+
* - Prevents the producer from racing ahead of the consumer if `maxUnread` is reached
97+
*
98+
* ### Error Handling
99+
* The mapper should ideally handle all errors internally to enable error handling
100+
* closest to where they occur. However, if errors do escape the mapper:
101+
*
102+
* When `stopOnMapperError` is true (default):
103+
* - First error immediately stops processing
104+
* - Error is thrown from the `AsyncIterator`'s next() call
105+
*
106+
* When `stopOnMapperError` is false:
107+
* - Processing continues despite errors
108+
* - All errors are collected and thrown together
109+
* - Errors are thrown as `AggregateError` after all items complete
110+
*
111+
* ### Usage
112+
* - Items are exposed to the `mapper` via an iterator or async iterator (this includes generator and async generator functions)
113+
* - IMPORTANT: `mapper` method not be invoked when `maxUnread` is reached, until items are consumed
114+
* - The iterable will set `done` when the `input` has indicated `done` and all `mapper` promises have resolved
115+
*
116+
* @example
95117
*
96118
* Consider a typical processing loop without IterableMapper:
119+
*
97120
* ```typescript
98121
* const source = new SomeSource();
99122
* const sourceIds = [1, 2,... 1000];
@@ -109,43 +132,81 @@ type NewElementOrError<NewElement = unknown> = {
109132
* We could prefetch the next read (300ms) while processing (20ms) and writing (500ms),
110133
* without changing the order of reads or writes.
111134
*
112-
* Using IterableMapper as a prefetcher:
135+
* @example
136+
*
137+
* Using `IterableMapper` as a prefetcher and blocking writes, without changing the order of reads or writes:
138+
*
113139
* ```typescript
114140
* const source = new SomeSource();
115141
* const sourceIds = [1, 2,... 1000];
116142
* // Pre-reads up to 8 items serially and releases in sequential order
117143
* const sourcePrefetcher = new IterableMapper(sourceIds,
118144
* async (sourceId) => source.read(sourceId),
119-
* { concurrency: 1 }
145+
* { concurrency: 1, maxUnread: 10 }
120146
* );
121147
* const sink = new SomeSink();
122-
* for await (const item of sourcePrefetcher) {
148+
* for await (const item of sourcePrefetcher) { // may not block for fast sources
123149
* const outputItem = doSomeOperation(item); // takes 20 ms of CPU
124150
* await sink.write(outputItem); // takes 500 ms of I/O wait, no CPU
125151
* }
126152
* ```
127153
*
128154
* This reduces iteration time to 520ms by overlapping reads with processing/writing.
129155
*
130-
* For maximum throughput, make the writes concurrent with
131-
* IterableQueueMapper (to iterate results with backpressure when too many unread items) or
132-
* IterableQueueMapperSimple (to handle errors at end without custom iteration or backpressure):
156+
* @example
157+
*
158+
* Using `IterableMapper` as a prefetcher with background writes, without changing the order of reads or writes:
133159
*
134160
* ```typescript
135161
* const source = new SomeSource();
136162
* const sourceIds = [1, 2,... 1000];
137163
* const sourcePrefetcher = new IterableMapper(sourceIds,
138164
* async (sourceId) => source.read(sourceId),
165+
* { concurrency: 1, maxUnread: 10 }
166+
* );
167+
* const sink = new SomeSink();
168+
* const flusher = new IterableQueueMapperSimple(
169+
* async (outputItem) => sink.write(outputItem),
139170
* { concurrency: 1 }
140171
* );
172+
* for await (const item of sourcePrefetcher) { // may not block for fast sources
173+
* const outputItem = doSomeOperation(item); // takes 20 ms of CPU
174+
* await flusher.enqueue(outputItem); // will periodically block for portion of write time
175+
* }
176+
* // Wait for all writes to complete
177+
* await flusher.onIdle();
178+
* // Check for errors
179+
* if (flusher.errors.length > 0) {
180+
* // ...
181+
* }
182+
* ```
183+
*
184+
* This reduces iteration time to about to `max((max(readTime, writeTime) - cpuOpTime, cpuOpTime))
185+
* by overlapping reads and writes with the CPU processing step.
186+
* In this contrived example, the loop time is reduced to 500ms - 20ms = 480ms.
187+
* In cases where the CPU usage time is higher, the impact can be greater.
188+
*
189+
* @example
190+
*
191+
* For maximum throughput, allow out of order reads and writes with
192+
* `IterableQueueMapper` (to iterate results with backpressure when too many unread items) or
193+
* `IterableQueueMapperSimple` (to handle errors at end without custom iteration and applying backpressure to block further enqueues when `concurrency` items are in process):
194+
*
195+
* ```typescript
196+
* const source = new SomeSource();
197+
* const sourceIds = [1, 2,... 1000];
198+
* const sourcePrefetcher = new IterableMapper(sourceIds,
199+
* async (sourceId) => source.read(sourceId),
200+
* { concurrency: 10, maxUnread: 20 }
201+
* );
141202
* const sink = new SomeSink();
142203
* const flusher = new IterableQueueMapperSimple(
143204
* async (outputItem) => sink.write(outputItem),
144205
* { concurrency: 10 }
145206
* );
146-
* for await (const item of sourcePrefetcher) {
207+
* for await (const item of sourcePrefetcher) { // typically will not block
147208
* const outputItem = doSomeOperation(item); // takes 20 ms of CPU
148-
* await flusher.enqueue(outputItem); // usually takes no time
209+
* await flusher.enqueue(outputItem); // typically will not block
149210
* }
150211
* // Wait for all writes to complete
151212
* await flusher.onIdle();
@@ -184,19 +245,6 @@ export class IterableMapper<Element, NewElement> implements AsyncIterable<NewEle
184245
* @param mapper Function called for every item in `input`. Returns a `Promise` or value.
185246
* @param options IterableMapper options
186247
*
187-
* Error Handling:
188-
* The mapper should ideally handle all errors internally to enable error handling
189-
* closest to where they occur. However, if errors do escape the mapper:
190-
*
191-
* When stopOnMapperError is true (default):
192-
* - First error immediately stops processing
193-
* - Error is thrown from the AsyncIterator's next() call
194-
*
195-
* When stopOnMapperError is false:
196-
* - Processing continues despite errors
197-
* - All errors are collected and thrown together
198-
* - Errors are thrown as AggregateError after all items complete
199-
*
200248
* @see {@link IterableQueueMapper} for full class documentation
201249
*/
202250
constructor(

src/iterable-queue-mapper-simple.ts

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,55 @@
1-
import { Mapper } from './iterable-mapper';
1+
import { IterableMapperOptions, Mapper } from './iterable-mapper';
22
import { IterableQueueMapper } from './iterable-queue-mapper';
33

44
// eslint-disable-next-line @typescript-eslint/no-explicit-any
55
type Errors<T> = { item: T; error: string | { [key: string]: any } | Error }[];
66

77
const NoResult = Symbol('noresult');
88

9+
/**
10+
* Options for IterableQueueMapperSimple
11+
*/
12+
export type IterableQueueMapperSimpleOptions = Pick<IterableMapperOptions, 'concurrency'>;
13+
914
/**
1015
* Accepts queue items via `enqueue` and calls the `mapper` on them
11-
* with specified concurrency, storing the
12-
* `mapper` result in a queue of specified max size, before
13-
* being iterated / read by the caller. The `enqueue` method will block if
14-
* the queue is full, until an item is read.
16+
* with specified `concurrency`, discards the results, and accumulates
17+
* exceptions in the `errors` property. When empty, `await enqueue()`
18+
* will return immediately, but when `concurrency` items are in progress,
19+
* `await enqueue()` will block until a slot is available to accept the item.
1520
*
1621
* @remarks
1722
*
18-
* Note: the name is somewhat of a misnomer as this wraps `IterableQueueMapper`
19-
* but is not itself an `Iterable`.
23+
* ### Typical Use Case
24+
* - Pushing items to an async I/O destination
25+
* - In the simple sequential (`concurrency: 1`) case, allows 1 item to be flushed async while caller prepares next item
26+
* - Results of the flushed items are not needed in a subsequent step (if they are, use `IterableQueueMapper`)
27+
*
28+
* ### Error Handling
29+
* The mapper should ideally handle all errors internally to enable error handling
30+
* closest to where they occur. However, if errors do escape the mapper:
31+
* - Processing continues despite errors
32+
* - All errors are collected in the `errors` property
33+
* - Errors can be checked/handled during processing via the `errors` property
2034
*
21-
* Accepts items for mapping in the background, discards the results,
22-
* but accumulates exceptions in the `errors` property.
35+
* Key Differences from `IterableQueueMapper`:
36+
* - `maxUnread` defaults to equal `concurrency` (simplifying queue management)
37+
* - Results are automatically iterated and discarded (all work should happen in mapper)
38+
* - Errors are collected rather than thrown (available via errors property)
2339
*
24-
* Allows up to `concurrency` mappers to be in progress before
25-
* `enqueue` will block until a mapper completes.
40+
* ### Usage
41+
* - Items are added to the queue via the `await enqueue()` method
42+
* - Check `errors` property to see if any errors occurred, stop if desired
43+
* - IMPORTANT: `await enqueue()` method will block until a slot is available, if queue is full
44+
* - IMPORTANT: Always `await onIdle()` to ensure all items are processed
45+
*
46+
* Note: the name is somewhat of a misnomer as this wraps `IterableQueueMapper`
47+
* but is not itself an `Iterable`.
2648
*
2749
* @category Enqueue Input
50+
*
51+
* @see {@link IterableQueueMapper} for related class with more configuration options
52+
* @see {@link IterableMapper} for underlying mapper implementation and examples of combined usage
2853
*/
2954
export class IterableQueueMapperSimple<Element> {
3055
private readonly _writer: IterableQueueMapper<Element, typeof NoResult>;
@@ -34,29 +59,17 @@ export class IterableQueueMapperSimple<Element> {
3459
private _isIdle = false;
3560

3661
/**
37-
* Create a new `IterableQueueMapperSimple`
62+
* Create a new `IterableQueueMapperSimple`, which uses `IterableQueueMapper` underneath, but
63+
* automatically iterates and discards results as they complete.
3864
*
39-
* @param mapper Function which is called for every item in `input`.
40-
* Expected to return a `Promise` or value.
41-
*
42-
* The `mapper` *should* handle all errors and not allow an error to be thrown
43-
* out of the `mapper` function as this enables the best handling of errors
44-
* closest to the time that they occur.
45-
*
46-
* If the `mapper` function does allow an error to be thrown then the
47-
* errors will be accumulated in the `errors` property.
65+
* @param mapper Function called for every enqueued item. Returns a `Promise` or value.
4866
* @param options IterableQueueMapperSimple options
67+
*
68+
* @see {@link IterableQueueMapperSimple} for full class documentation
69+
* @see {@link IterableQueueMapper} for related class with more configuration options
70+
* @see {@link IterableMapper} for underlying mapper implementation and examples of combined usage
4971
*/
50-
constructor(
51-
mapper: Mapper<Element, void>,
52-
options: {
53-
/**
54-
* Number of items to accept for mapping before requiring the caller to wait for one to complete.
55-
* @default 4
56-
*/
57-
concurrency?: number;
58-
} = {},
59-
) {
72+
constructor(mapper: Mapper<Element, void>, options: IterableQueueMapperSimpleOptions = {}) {
6073
const { concurrency = 4 } = options;
6174

6275
this._mapper = mapper;
@@ -106,7 +119,7 @@ export class IterableQueueMapperSimple<Element> {
106119
/**
107120
* Accept a request for sending in the background if a concurrency slot is available.
108121
* Else, do not return until a concurrency slot is freed up.
109-
* This provides concurrency background writes with back pressure to prevent
122+
* This provides concurrency background writes with backpressure to prevent
110123
* the caller from getting too far ahead.
111124
*
112125
* MUST await `onIdle` for background `mappers`s to finish

src/iterable-queue-mapper.ts

Lines changed: 38 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,65 @@
11
//
22
// 2021-08-25 - Initially based on: https://raw.githubusercontent.com/sindresorhus/p-map/main/index.js
33
//
4-
import { IterableMapper, Mapper } from './iterable-mapper';
4+
import { IterableMapper, IterableMapperOptions, Mapper } from './iterable-mapper';
55
import { IterableQueue } from './iterable-queue';
66

7-
export interface IterableQueueMapperOptions {
8-
/**
9-
* Number of concurrently pending promises returned by `mapper`.
10-
*
11-
* Must be an integer from 1 and up or `Infinity`, must be <= `maxUnread`.
12-
*
13-
* @default 4
14-
*/
15-
readonly concurrency?: number;
16-
17-
/**
18-
* Number of pending unread iterable items.
19-
*
20-
* Must be an integer from 1 and up or `Infinity`, must be >= `concurrency`.
21-
*
22-
* @default 8
23-
*/
24-
readonly maxUnread?: number;
25-
26-
/**
27-
* When set to `false`, instead of stopping when a promise rejects, it will wait for all the promises to settle and then reject with an [aggregated error](https://github.com/sindresorhus/aggregate-error) containing all the errors from the rejected promises.
28-
*
29-
* @default true
30-
*/
31-
readonly stopOnMapperError?: boolean;
32-
}
7+
/**
8+
* Options for IterableQueueMapper
9+
*/
10+
export type IterableQueueMapperOptions = IterableMapperOptions;
3311

3412
/**
3513
* Accepts queue items via `enqueue` and calls the `mapper` on them
36-
* with specified concurrency, storing the
37-
* `mapper` result in a queue of specified max size, before
38-
* being iterated / read by the caller. The `enqueue` method will block if
39-
* the queue is full, until an item is read.
14+
* with specified `concurrency`, storing the `mapper` result in a queue
15+
* of `maxUnread` size, before being iterated / read by the caller.
16+
* The `enqueue` method will block if the queue is full, until an item is read.
4017
*
4118
* @remarks
4219
*
43-
* This allows performing a concurrent mapping with
44-
* back pressure for items added after queue creation
45-
* via a method call.
20+
* ### Typical Use Case
21+
* - Pushing items to an async I/O destination
22+
* - In the simple sequential (`concurrency: 1`) case, allows 1 item to be flushed async while caller prepares next item
23+
* - Results of the flushed items are needed in a subsequent step (if they are not, use `IterableQueueMapperSimple`)
24+
* - Prevents the producer from racing ahead of the consumer if `maxUnread` is reached
25+
*
26+
* ### Error Handling
27+
* The mapper should ideally handle all errors internally to enable error handling
28+
* closest to where they occur. However, if errors do escape the mapper:
4629
*
47-
* Because items are added via a method call it is possible to
48-
* chain an `IterableMapper` that prefetches files and processes them,
49-
* with an `IterableQueueMapper` that processes the results of the
50-
* `mapper` function of the `IterableMapper`.
30+
* When `stopOnMapperError` is true (default):
31+
* - First error immediately stops processing
32+
* - Error is thrown from the `AsyncIterator`'s next() call
5133
*
52-
* Typical use case is for a `background uploader` that prevents
53-
* the producer from racing ahead of the upload process, consuming
54-
* too much memory or disk space. As items are ready for upload
55-
* they are added to the queue with the `enqueue` method, which is
56-
* `await`ed by the caller. If the queue has room then `enqueue`
57-
* will return immediately, otherwise it will block until there is room.
34+
* When `stopOnMapperError` is false:
35+
* - Processing continues despite errors
36+
* - All errors are collected and thrown together
37+
* - Errors are thrown as `AggregateError` after all items complete
38+
*
39+
* ### Usage
40+
* - Items are added to the queue via the `await enqueue()` method
41+
* - IMPORTANT: `await enqueue()` method will block until a slot is available, if queue is full
42+
* - Call `done()` when no more items will be enqueued
43+
* - IMPORTANT: Always `await onIdle()` to ensure all items are processed
5844
*
5945
* @category Enqueue Input
46+
*
47+
* @see {@link IterableMapper} for underlying mapper implementation and examples of combined usage
6048
*/
6149
export class IterableQueueMapper<Element, NewElement> implements AsyncIterable<NewElement> {
6250
private _iterableMapper: IterableMapper<Element, NewElement>;
6351

6452
private _sourceIterable: IterableQueue<Element>;
6553

6654
/**
67-
* Create a new `IterableQueueMapper`
68-
*
69-
* @param mapper Function which is called for every item in `input`.
70-
* Expected to return a `Promise` or value.
55+
* Create a new `IterableQueueMapper`, which uses `IterableMapper` underneath, and exposes a
56+
* queue interface for adding items that are not exposed via an iterator.
7157
*
72-
* The `mapper` *should* handle all errors and not allow an error to be thrown
73-
* out of the `mapper` function as this enables the best handling of errors
74-
* closest to the time that they occur.
75-
*
76-
* If the `mapper` function does allow an error to be thrown then the
77-
* `stopOnMapperError` option controls the behavior:
78-
* - `stopOnMapperError`: `true` - will throw the error
79-
* out of `next` or the `AsyncIterator` returned from `[Symbol.asyncIterator]`
80-
* and stop processing.
81-
* - `stopOnMapperError`: `false` - will continue processing
82-
* and accumulate the errors to be thrown from `next` or the `AsyncIterator`
83-
* returned from `[Symbol.asyncIterator]` when all items have been processed.
58+
* @param mapper Function called for every enqueued item. Returns a `Promise` or value.
8459
* @param options IterableQueueMapper options
60+
*
61+
* @see {@link IterableQueueMapper} for full class documentation
62+
* @see {@link IterableMapper} for underlying mapper implementation and examples of combined usage
8563
*/
8664
constructor(mapper: Mapper<Element, NewElement>, options: IterableQueueMapperOptions = {}) {
8765
this._sourceIterable = new IterableQueue({

0 commit comments

Comments
 (0)