@johnhenry/iteration 0.0.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.
Files changed (88) hide show
  1. package/MIT-LICENSE.txt +20 -0
  2. package/dist/abort.d.ts +26 -0
  3. package/dist/abort.d.ts.map +1 -0
  4. package/dist/abort.js +69 -0
  5. package/dist/abort.js.map +1 -0
  6. package/dist/assertions/eventualequal.d.ts +9 -0
  7. package/dist/assertions/eventualequal.d.ts.map +1 -0
  8. package/dist/assertions/eventualequal.js +30 -0
  9. package/dist/assertions/eventualequal.js.map +1 -0
  10. package/dist/async-channel.d.ts +101 -0
  11. package/dist/async-channel.d.ts.map +1 -0
  12. package/dist/async-channel.js +208 -0
  13. package/dist/async-channel.js.map +1 -0
  14. package/dist/channel-decorators.d.ts +23 -0
  15. package/dist/channel-decorators.d.ts.map +1 -0
  16. package/dist/channel-decorators.js +31 -0
  17. package/dist/channel-decorators.js.map +1 -0
  18. package/dist/concurrency.d.ts +43 -0
  19. package/dist/concurrency.d.ts.map +1 -0
  20. package/dist/concurrency.js +145 -0
  21. package/dist/concurrency.js.map +1 -0
  22. package/dist/consumers.d.ts +199 -0
  23. package/dist/consumers.d.ts.map +1 -0
  24. package/dist/consumers.js +349 -0
  25. package/dist/consumers.js.map +1 -0
  26. package/dist/count.d.ts +37 -0
  27. package/dist/count.d.ts.map +1 -0
  28. package/dist/count.js +70 -0
  29. package/dist/count.js.map +1 -0
  30. package/dist/empty-iterator.d.ts +15 -0
  31. package/dist/empty-iterator.d.ts.map +1 -0
  32. package/dist/empty-iterator.js +16 -0
  33. package/dist/empty-iterator.js.map +1 -0
  34. package/dist/eventualequal.d.ts +2 -0
  35. package/dist/eventualequal.d.ts.map +1 -0
  36. package/dist/eventualequal.js +54 -0
  37. package/dist/eventualequal.js.map +1 -0
  38. package/dist/exhaust.d.ts +7 -0
  39. package/dist/exhaust.d.ts.map +1 -0
  40. package/dist/exhaust.js +22 -0
  41. package/dist/exhaust.js.map +1 -0
  42. package/dist/index.d.ts +16 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +16 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/is-iterator.d.ts +5 -0
  47. package/dist/is-iterator.d.ts.map +1 -0
  48. package/dist/is-iterator.js +5 -0
  49. package/dist/is-iterator.js.map +1 -0
  50. package/dist/iterator-tools.d.ts +164 -0
  51. package/dist/iterator-tools.d.ts.map +1 -0
  52. package/dist/iterator-tools.js +231 -0
  53. package/dist/iterator-tools.js.map +1 -0
  54. package/dist/itertools.d.ts +237 -0
  55. package/dist/itertools.d.ts.map +1 -0
  56. package/dist/itertools.js +471 -0
  57. package/dist/itertools.js.map +1 -0
  58. package/dist/tee.d.ts +18 -0
  59. package/dist/tee.d.ts.map +1 -0
  60. package/dist/tee.js +118 -0
  61. package/dist/tee.js.map +1 -0
  62. package/dist/transduce.d.ts +79 -0
  63. package/dist/transduce.d.ts.map +1 -0
  64. package/dist/transduce.js +37 -0
  65. package/dist/transduce.js.map +1 -0
  66. package/dist/transducers.d.ts +123 -0
  67. package/dist/transducers.d.ts.map +1 -0
  68. package/dist/transducers.js +212 -0
  69. package/dist/transducers.js.map +1 -0
  70. package/package.json +122 -0
  71. package/readme.md +761 -0
  72. package/src/abort.ts +80 -0
  73. package/src/assertions/eventualequal.ts +36 -0
  74. package/src/async-channel.ts +228 -0
  75. package/src/channel-decorators.ts +49 -0
  76. package/src/concurrency.ts +168 -0
  77. package/src/consumers.ts +425 -0
  78. package/src/count.ts +72 -0
  79. package/src/empty-iterator.ts +17 -0
  80. package/src/eventualequal.ts +54 -0
  81. package/src/exhaust.ts +26 -0
  82. package/src/index.ts +17 -0
  83. package/src/is-iterator.ts +9 -0
  84. package/src/iterator-tools.ts +290 -0
  85. package/src/itertools.ts +518 -0
  86. package/src/tee.ts +136 -0
  87. package/src/transduce.ts +161 -0
  88. package/src/transducers.ts +256 -0
package/readme.md ADDED
@@ -0,0 +1,761 @@
1
+ # @johnhenry/iteration
2
+
3
+ This module implements a number of asynchronous iterator building blocks inspired by constructs from [Python](https://docs.python.org/3/library/itertools.html), APL, Haskell, and SML. Each has been recast in a form suitable for JavaScript.
4
+
5
+ The module standardizes a core set of fast, memory efficient tools that are useful by themselves or in combination. Together, they form an “iterator algebra” making it possible to construct specialized tools succinctly and efficiently in pure JavaScript.
6
+
7
+ ### Native Iterator Helpers interop
8
+
9
+ Since this library's original release, JavaScript gained native **Iterator Helpers** (`Iterator.prototype.map/filter/take/drop/flatMap/reduce/toArray/forEach/some/every/find`, `Iterator.from`, shipped as ES2025 in Node 22+ and current Chrome/Firefox/Safari) — covering much of this library's original *synchronous* transformation story directly in the language. Where a sync function here is fully covered by a native helper, its implementation leans on `Iterator.from(iterable)` rather than a hand-rolled generator.
10
+
11
+ **Async Iterator helpers are not yet shipped** (still TC39 Stage 2 as of this writing), so the async half of this library remains hand-rolled and is where it continues to add the most value — every sync function here has an async dual for exactly this reason. See [docs/discussion/python-itertools.md](./docs/discussion/python-itertools.md) for the full design rationale and a function-by-function comparison to Python's `itertools`.
12
+
13
+ Requires **Node.js 22.12+** (or an equivalent Iterator-Helpers-capable engine).
14
+
15
+ > **Provenance.** This library has been renamed twice, so its published history spans three names.
16
+ > It started life as [`async-itertools`](https://github.com/johnhenry/async-itertools) (now
17
+ > archived) — the 1.x releases and the "2.0" milestones described below (transducer rewrite,
18
+ > cancellation, bounded concurrency, backpressure) all happened under that name. It was then
19
+ > absorbed into a pure-TypeScript monorepo as `mallory-iteration`, with full git history preserved
20
+ > via `git subtree`. It now ships as **`@johnhenry/iteration`** from the
21
+ > [`johnhenry/math`](https://github.com/johnhenry/math) monorepo, restarted at `0.0.0` under the
22
+ > new scoped name — the version number is fresh, the code and its lineage are not.
23
+
24
+ ## What's new in 2.0
25
+
26
+ - **Transducer protocol rewritten (memory-leak fix).** In the 1.x line the
27
+ transduce engine threaded its accumulator as an iterable that every step
28
+ wrapped in a fresh generator (`conjoin(init, item)`), retaining one
29
+ generator object *per item processed* (~176 bytes/item) — unbounded heap
30
+ growth on long streams. In 2.0 the accumulator is a plain array used as a
31
+ **pending-emission buffer**: the innermost step pushes emitted items onto
32
+ it and `reduceSync`/`reduceAsync` drain it after each step, so memory
33
+ stays flat no matter how many items flow through. Observable behavior of
34
+ `transduceSync`/`transduceAsync` and every built-in transducer is
35
+ unchanged (composition order, the `.complete` flush protocol, and HALT
36
+ early termination all work as before) — but **custom code that
37
+ implemented the old step protocol directly** (calling
38
+ `conjoinSync`/`conjoinAsync` as its inner reducer, or passing iterable
39
+ accumulators to `reduceSync`/`reduceAsync`) must be updated: a step now
40
+ receives the buffer array, calls its inner step (which pushes), and
41
+ returns the buffer (or `HALT`). See `ReducerStep`/`Transducer` in
42
+ `src/iterator-tools.ts`.
43
+ - **`HAULT` → `HALT`.** The early-termination sentinel is now spelled
44
+ `HALT`; `HAULT` remains exported as a deprecated alias (same symbol).
45
+ - **TypeScript.** The library is now written in TypeScript and ships
46
+ compiled ESM + `.d.ts` declarations from `dist/`. The transducer protocol
47
+ is fully typed (`Transducer<In, Out>`), still with **zero runtime
48
+ dependencies**.
49
+ - **AbortSignal support.** Async terminal consumers and `transduceAsync`
50
+ accept an optional `{ signal }` (see below).
51
+ - **Bounded concurrency.** New `mapConcurrentAsync` and `prefetchAsync`
52
+ (see below).
53
+ - **AsyncChannel backpressure.** `put()` now returns a promise that waits
54
+ for capacity instead of throwing `"cache full"` (see below).
55
+
56
+ ### Naming conventions (worth knowing before you alias/re-export)
57
+
58
+ Some names here are deliberate and easy to misread; downstream consumers
59
+ who remap names should keep these distinctions:
60
+
61
+ - **`countSync`/`countAsync`** (and `countBig*`) are **integer sequence
62
+ generators**, mirroring Python's `itertools.count` — not "count the
63
+ items" (that's `quantifySync`/`quantifyAsync`).
64
+ - **`reduceSync`/`reduceAsync`** are the **streaming primitive** under
65
+ `transduce*` (they yield each emitted item); the **terminal**
66
+ reduce-to-one-value operation is **`foldSync`/`foldAsync`**.
67
+ - **`group(n)`** (transducer) chunks by **fixed size**;
68
+ **`groupBySync`/`groupByAsync`** (and the `partitionBy` transducer)
69
+ group **consecutive runs by key**.
70
+
71
+ ### CommonJS / `require()`
72
+
73
+ This package ships compiled ESM (`"type": "module"`, `dist/*.js` built from
74
+ TypeScript source) — but it's still directly usable from CommonJS. Node's
75
+ native `require(esm)` support (stable and unflagged since **Node 22.12**,
76
+ which is why that's this package's floor) lets `require()` load an ES module
77
+ synchronously:
78
+
79
+ ```javascript
80
+ const { countSync, someAsync } = require("@johnhenry/iteration");
81
+ ```
82
+
83
+ No separate CJS build or `"require"` condition in `package.json`'s `exports` is needed or provided — `require(esm)` resolves through the same ESM files everything else uses. This is verified in CI (`scripts/require-esm-smoke-test.cjs`), not just documented.
84
+
85
+ ## Installation
86
+
87
+ ```bash
88
+ npm install @johnhenry/iteration
89
+ ```
90
+
91
+ ## Production
92
+
93
+ Generally, you'll use this library to transform existing iterators; but we provide a number of differerent methods to create iterators as well.
94
+
95
+ ### `emptySync` & `emptyAsync`
96
+
97
+ Create empty iterators
98
+
99
+ ```javascript
100
+ import { emptySync, emptyAsync } from "@johnhenry/iteration";
101
+ for (const a of emptySync()) {
102
+ // dream the impossible
103
+ }
104
+ for (const a of emptyAsync()) {
105
+ // do the impossible!
106
+ }
107
+ ```
108
+
109
+ ### `countSync` & `countBigSync`; `countAsync` & `countBigAsync`
110
+
111
+ Create a sequence of integers
112
+
113
+ ```javascript
114
+ import {
115
+ countSync,
116
+ countBigSync,
117
+ countAsync,
118
+ countBigAsync,
119
+ } from "@johnhenry/iteration";
120
+ for (const a of countSync(0, 9)) {
121
+ console.log(a);
122
+ } // logs 1, 2, 3, 4, 5, 6, 7, 8, 9
123
+ for (const a of countBigSync(9n, 0n)) {
124
+ console.log(a);
125
+ } // logs 9n, 8n, 7n, 6n, 5n, 4n, 3n, 2n, 1n, 0n
126
+ for await (const a of countAsync(0, 9)) {
127
+ console.log(a);
128
+ } // logs 1, 2, 3, 4, 5, 6, 7, 8, 9
129
+ for await (const a of countBigAsync(9n, 0n)) {
130
+ console.log(a);
131
+ } // logs 9n, 8n, 7n, 6n, 5n, 4n, 3n, 2n, 1n, 0n
132
+ ```
133
+
134
+ ### `syncFrom` & `asyncFrom`
135
+
136
+ Create iterators from given items
137
+
138
+ ```javascript
139
+ import { syncFrom, asyncFrom } from "@johnhenry/iteration";
140
+ for (const a of syncFrom(1, 2, 3)) {
141
+ console.log(a);
142
+ } // logs 1, 2, 3
143
+ for await (const a of asyncFrom(4, 5, 6)) {
144
+ console.log(a);
145
+ } // logs 4, 5, 6
146
+ ```
147
+
148
+ ### `zipSync` & `zipAsync`
149
+
150
+ Zip several iterators together, stopping at the shortest one. `zipAsync` pulls
151
+ the next value from every input in parallel each round, and accepts a mix of
152
+ sync and async iterables.
153
+
154
+ ```javascript
155
+ import { zipSync, zipAsync } from "@johnhenry/iteration";
156
+ for (const pair of zipSync([1, 2, 3], ["a", "b", "c"])) {
157
+ console.log(pair);
158
+ } // logs [1,'a'], [2,'b'], [3,'c']
159
+ for await (const pair of zipAsync([1, 2, 3], ["a", "b"])) {
160
+ console.log(pair);
161
+ } // logs [1,'a'], [2,'b'] -- stops at the shorter input
162
+ ```
163
+
164
+ ## Transformation: transducers
165
+
166
+ This library employs transducers,
167
+ (see [this](https://clojure.org/reference/transducers)),
168
+ to transform iterators.
169
+
170
+ This library provides two methods
171
+ -- `transduceSync` and `transduceAsync` --
172
+ to apply transducers to synchronous
173
+ and asynchronous iterators.
174
+
175
+ ```javascript
176
+ import { transduceSync } from "@johnhenry/iteration";
177
+ // import { transduceSync } from "@johnhenry/iteration/transduce";
178
+ for (const item of transduceSync(/*list of transducers*/)(/*some iterator*/)) {
179
+ // do something with transduced item
180
+ }
181
+ ```
182
+
183
+ ```javascript
184
+ import { transduceAsync } from "@johnhenry/iteration";
185
+ // import { transduceAsync } from "@johnhenry/iteration/transduce";
186
+ for await (const item of transduceAsync(/*list of transducers*/)(/*some asynchronous iterator*/)) {
187
+ // do something with transduced item
188
+ }
189
+ ```
190
+
191
+ In addition, the library provides
192
+ a number of built-in transducers
193
+ that can be applied.
194
+
195
+ ### `map`
196
+
197
+ Similiar to [Array.prototype.map](),
198
+ maps items with a given transformation function.
199
+
200
+ ```javascript
201
+ // import { transducers } from "@johnhenry/iteration";
202
+ // const { map } = transducers;
203
+ import { map } from "@johnhenry/iteration/transducers";
204
+ const addOne = map((x) => x + 1);
205
+ const abs = map(Math.abs);
206
+ for (const x of transduceSync(addOne, abs)([-3, -2, -1, 0, 1, 2, 3])) {
207
+ console.log(x);
208
+ }
209
+ // logs: 2, 1, 0, 1, 2, 3, 4
210
+ ```
211
+
212
+ ### `filter`
213
+
214
+ Similiar to [Array.prototype.filter](),
215
+ filters items that do not match a given predicate
216
+
217
+ ```javascript
218
+ import { filter } from "@johnhenry/iteration/transducers";
219
+ const removeStrings = map((x) => typeof x !== "string");
220
+ const keepPositive = map((x) => x > 0);
221
+ for (const x of transduceSync(
222
+ removeStrings,
223
+ keepPositive
224
+ )(["alice", -3, -2, "bob", -1, 0, 1, "claire", 2, 3])) {
225
+ console.log(x);
226
+ }
227
+ // logs: 0, 1, 2, 3
228
+ ```
229
+
230
+ ### `accumulate`
231
+
232
+ Apply function successively to items in iterator.
233
+ Similar to `Array.prototype.reduce`.
234
+
235
+ ```javascript
236
+ import { accumulate } from "@johnhenry/iteration/transducers";
237
+ const sum = accumulate((a, b) => a + b, 0);
238
+ for (const x of transduceSync(sum)([1, 2, 3, 4])) {
239
+ console.log(x);
240
+ }
241
+ // logs: 1, 3, 6, 10
242
+ ```
243
+
244
+ ### `group`
245
+
246
+ Place items into groups of size N. A trailing, under-sized group is flushed
247
+ once the source is exhausted (via the transducer completion protocol — see
248
+ `transduceSync`/`transduceAsync`).
249
+
250
+ ```javascript
251
+ import { group } from "@johnhenry/iteration/transducers";
252
+ const triplet = group(3);
253
+ for (const x of transduceSync(triplet)([1, 2, 3, 4, 5, 6, 7])) {
254
+ console.log(x);
255
+ }
256
+ // logs: [1, 2, 3], [4, 5, 6], [7]
257
+ ```
258
+
259
+ ### `take`
260
+
261
+ Take only the first N items and drop the rest (see 'drop').
262
+
263
+ ```javascript
264
+ import { take } from "@johnhenry/iteration/transducers";
265
+ const take5 = take(5);
266
+ for (const x of transduceSync(take5)([1, 2, 3, 4, 5, 6, 7, 8, 9])) {
267
+ console.log(x);
268
+ }
269
+ // logs: 1, 2, 3, 4, 5
270
+ ```
271
+
272
+ ### `drop`
273
+
274
+ Drop the first N items and take the rest (see 'take'). Previously named
275
+ `reject` — renamed since `reject` now names the predicate-based complement
276
+ of `filter`, below.
277
+
278
+ ```javascript
279
+ import { drop } from "@johnhenry/iteration/transducers";
280
+ const dropDozen = drop(12);
281
+ for (const x of transduceSync(dropDozen)([
282
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
283
+ ])) {
284
+ console.log(x);
285
+ }
286
+ // logs 13, 14
287
+ ```
288
+
289
+ ### `reject`
290
+
291
+ Reject items matching a predicate and keep the rest — the complement of
292
+ `filter` (see 'filter'). Mirrors Python's `itertools.filterfalse`.
293
+
294
+ ```javascript
295
+ import { reject } from "@johnhenry/iteration/transducers";
296
+ const rejectEven = reject((x) => x % 2 === 0);
297
+ for (const x of transduceSync(rejectEven)([1, 2, 3, 4, 5])) {
298
+ console.log(x);
299
+ }
300
+ // logs 1, 3, 5
301
+ ```
302
+
303
+ ### `dedupe`
304
+
305
+ Skip consecutive duplicate items (by key). Mirrors the `unique_justseen`
306
+ recipe documented in Python's itertools docs — the transducer-pipeline,
307
+ adjacent-only counterpart to `uniqueSync`/`uniqueAsync` (which dedupe
308
+ globally, across the whole stream, not just neighboring items).
309
+
310
+ ```javascript
311
+ import { dedupe } from "@johnhenry/iteration/transducers";
312
+ for (const x of transduceSync(dedupe())([1, 1, 2, 2, 1, 1, 3])) {
313
+ console.log(x);
314
+ }
315
+ // logs 1, 2, 1, 3 -- note the non-adjacent 1s both survive
316
+ ```
317
+
318
+ ### `interpose`
319
+
320
+ Insert a separator between consecutive emitted items — not before the
321
+ first item, and not after the last.
322
+
323
+ ```javascript
324
+ import { interpose } from "@johnhenry/iteration/transducers";
325
+ for (const x of transduceSync(interpose(","))([1, 2, 3])) {
326
+ console.log(x);
327
+ }
328
+ // logs 1, ',', 2, ',', 3
329
+ ```
330
+
331
+ ### `partitionBy`
332
+
333
+ Group consecutive items sharing a key into arrays, emitting each completed
334
+ group as soon as the key changes. The transducer-pipeline counterpart to
335
+ `groupBySync`/`groupByAsync`; distinct from `group`, above, which chunks by
336
+ a fixed size rather than by a shared key. Like `group`, a trailing partial
337
+ run is flushed once the source completes.
338
+
339
+ ```javascript
340
+ import { partitionBy } from "@johnhenry/iteration/transducers";
341
+ for (const x of transduceSync(partitionBy())([1, 1, 2, 1, 1])) {
342
+ console.log(x);
343
+ }
344
+ // logs [1, 1], [2], [1, 1]
345
+ ```
346
+
347
+ ### `tap`
348
+
349
+ Call `fn(item)` for its side effect and pass the item through unchanged.
350
+ RxJS-style, for debugging or instrumenting a pipeline without altering its
351
+ values.
352
+
353
+ ```javascript
354
+ import { tap } from "@johnhenry/iteration/transducers";
355
+ const logged = tap((x) => console.log("saw:", x));
356
+ for (const x of transduceSync(logged)([1, 2, 3])) {
357
+ // "saw: 1", "saw: 2", "saw: 3" logged as a side effect;
358
+ // x is still 1, 2, 3
359
+ }
360
+ ```
361
+
362
+ Multiple different types of transducers can be applied.
363
+
364
+ ```javascript
365
+ import { map, filter, take } from "@johnhenry/iteration/transducers";
366
+ const transformation = transduceSync(
367
+ map((x) => x + 2),
368
+ filter((x) => x % 2),
369
+ take(4)
370
+ );
371
+ for (const x of transformation([
372
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
373
+ ])) {
374
+ console.log(x);
375
+ }
376
+ // logs 3, 5, 7, 9
377
+ ```
378
+
379
+ ### Early termination: `HALT`
380
+
381
+ A step may return the `HALT` sentinel to stop consumption of the source —
382
+ this is how `take` works. Renamed from the misspelled `HAULT` in 2.0;
383
+ `HAULT` is still exported as a deprecated alias of the same symbol.
384
+
385
+ ```javascript
386
+ import { HALT } from "@johnhenry/iteration";
387
+ ```
388
+
389
+ ### The step protocol (writing custom transducers)
390
+
391
+ As of 2.0, the accumulator is a plain array used as a *pending-emission
392
+ buffer*: the innermost step ("emit") pushes emitted items onto it and
393
+ returns it, and `reduceSync`/`reduceAsync` drain the buffer after each
394
+ step. A transducer transforms a step consuming its output type into a step
395
+ consuming its input type:
396
+
397
+ ```typescript
398
+ type Transducer<In, Out> = (next: ReducerStep<Out>) => ReducerStep<In>;
399
+ // a ReducerStep<In> is (buffer, item: In, iterator?) => buffer | HALT,
400
+ // optionally carrying a .complete(buffer) => buffer flush method
401
+ ```
402
+
403
+ Custom stateful transducers can flush buffered state once the source
404
+ iterator completes by attaching a `.complete(init)` method to the step
405
+ function they return (see `group`, above, for a worked example, and note
406
+ that `.complete` must cascade to the inner step's own `.complete`) — see
407
+ `reduceSync`/`reduceAsync` in `src/iterator-tools.ts` for the protocol.
408
+
409
+ > **Migrating from 2.0:** the old protocol threaded an *iterable*
410
+ > accumulator that each step wrapped in a new generator via
411
+ > `conjoin(init, item)` — which retained one generator per item processed
412
+ > and leaked ~176 bytes/item on long streams. If you wrote a custom
413
+ > transducer with the standard curried shape
414
+ > (`(conjoin) => (init, item) => conjoin(init, item)` etc.) it keeps
415
+ > working unchanged; only code that constructed accumulator iterables
416
+ > itself, or called `reduceSync`/`reduceAsync` with an iterable `init`,
417
+ > needs updating to the array-buffer protocol.
418
+
419
+ ## Python itertools parity (`@johnhenry/iteration/itertools`)
420
+
421
+ Flat, non-curried building blocks mirroring functions from Python's
422
+ [`itertools`](https://docs.python.org/3/library/itertools.html) module —
423
+ unlike the transducer factories above, these take the iterable(s) directly
424
+ and return a generator, matching Python's argument order. Each has a sync
425
+ and async form. See
426
+ [docs/discussion/python-itertools.md](./docs/discussion/python-itertools.md)
427
+ for the full comparison table and documented design divergences.
428
+
429
+ ```javascript
430
+ import {
431
+ takeWhileSync,
432
+ dropWhileSync,
433
+ compressSync,
434
+ pairwiseSync,
435
+ windowedSync,
436
+ groupBySync,
437
+ chainSync,
438
+ flattenSync,
439
+ cycleSync,
440
+ repeatSync,
441
+ uniqueSync,
442
+ enumerateSync,
443
+ starmapSync,
444
+ zipLongestSync,
445
+ isliceSync,
446
+ // ...and their `*Async` duals: takeWhileAsync, dropWhileAsync, etc.
447
+ } from "@johnhenry/iteration";
448
+ // or: import { ... } from "@johnhenry/iteration/itertools";
449
+
450
+ takeWhileSync((x) => x < 3, [1, 2, 3, 4, 1]); // yields 1, 2
451
+ dropWhileSync((x) => x < 3, [1, 2, 3, 4, 1]); // yields 3, 4, 1
452
+ compressSync(["a", "b", "c", "d"], [1, 0, 1, 0]); // yields 'a', 'c'
453
+ [...pairwiseSync([1, 2, 3, 4])]; // [[1,2],[2,3],[3,4]]
454
+ [...windowedSync([1, 2, 3, 4, 5], 3)]; // [[1,2,3],[2,3,4],[3,4,5]]
455
+ [...groupBySync([1, 1, 2, 1])]; // [[1,[1,1]],[2,[2]],[1,[1]]] -- consecutive runs only
456
+ [...chainSync([1, 2], [3, 4])]; // [1,2,3,4]
457
+ [...flattenSync([[1, 2], [3, 4]])]; // [1,2,3,4]
458
+ [...isliceSync(cycleSync([1, 2, 3]), 7)]; // [1,2,3,1,2,3,1]
459
+ [...repeatSync("x", 3)]; // ['x','x','x']
460
+ [...uniqueSync([1, 1, 2, 3, 2, 1])]; // [1,2,3]
461
+ [...enumerateSync(["a", "b"])]; // [[0,'a'],[1,'b']]
462
+ [...starmapSync((a, b) => a + b, [[1, 2], [3, 4]])]; // [3,7]
463
+ [...zipLongestSync(null, [1, 2, 3], ["a", "b"])]; // [[1,'a'],[2,'b'],[3,null]]
464
+ [...isliceSync([1, 2, 3, 4, 5, 6, 7, 8], 1, 8, 2)]; // [2,4,6,8]
465
+ ```
466
+
467
+ ## Combinatorics — moved to `@johnhenry/math`
468
+
469
+ `product`, `permutations`, `combinations`, and `combinationsWithReplacement`
470
+ now live in [`@johnhenry/math`](../math)'s `Combinatorics`, alongside the counting
471
+ functions they pair with (`binomial`, `permutationsCount`, …). Enumeration is
472
+ mathematics rather than stream plumbing, and having both faces in one place
473
+ lets the library assert that they agree.
474
+
475
+ ```javascript
476
+ import { Combinatorics } from "@johnhenry/math";
477
+
478
+ [...Combinatorics.combinations([1, 2, 3], 2)]; // [[1,2],[1,3],[2,3]]
479
+ Combinatorics.binomial(3, 2); // 3n — the count of exactly the above
480
+ ```
481
+
482
+ The `Async` duals were dropped rather than moved: they only wrapped
483
+ "materialize the source, then run the same sync generator" (these algorithms
484
+ are inherently collect-then-compute, in Python too), so `exhaustAsync` from
485
+ this package plus the sync generator gives the identical result.
486
+
487
+ ## Terminal consumers (`@johnhenry/iteration/consumers`)
488
+
489
+ Resolve an iterable to a single value (or a `Promise` of one), rather than
490
+ another iterable. Two families:
491
+
492
+ - **Iterator-Helper parity** — `someSync`/`everySync`/`findSync`/
493
+ `forEachSync`/`foldSync` mirror native ES2025 `Iterator.prototype.some/
494
+ every/find/forEach/reduce` (which sync code already gets for free via
495
+ `Iterator.from(iterable)`); the `*Async` duals hand-roll the same behavior
496
+ since `AsyncIterator` helpers aren't shipped yet. `foldSync`/`foldAsync`
497
+ are named `fold`, not `reduce`, to avoid colliding with this library's own
498
+ `reduceSync`/`reduceAsync` (an internal streaming primitive, not a
499
+ terminal reduce-to-one-value). `someSync`/`someAsync`/`everySync`/
500
+ `everyAsync`/`findSync`/`findAsync` all short-circuit — they won't
501
+ exhaust an infinite source once the answer is known.
502
+ - **Summary/aggregate consumers** — `firstSync`/`lastSync`/`nthSync`/
503
+ `quantifySync`/`minSync`/`maxSync`, borrowed from Python builtins/
504
+ itertools-recipes and other itertools-adjacent libraries' "summary"
505
+ namespaces. `quantifySync`/`quantifyAsync` (count items matching a
506
+ predicate) are named after the `quantify` recipe documented in Python's
507
+ own itertools docs — deliberately *not* `count`, which already means
508
+ something else here (`countSync` mirrors `itertools.count`, an integer
509
+ sequence generator; see [`countSync` & `countBigSync`](#countsync--countbigsync-countasync--countbigasync),
510
+ above). `findSync`/`firstSync`/`lastSync`/`nthSync`/`minSync`/`maxSync`
511
+ return `undefined` (or a caller-supplied `defaultValue`) on an
512
+ empty/no-match input rather than throwing — this deliberately diverges
513
+ from Python's own `min()`/`max()`, which throw without a `default`.
514
+
515
+ ```javascript
516
+ import {
517
+ someSync, everySync, findSync, forEachSync, foldSync,
518
+ firstSync, lastSync, nthSync, quantifySync, minSync, maxSync,
519
+ // ...and their `*Async` duals
520
+ } from "@johnhenry/iteration";
521
+ // or: import { ... } from "@johnhenry/iteration/consumers";
522
+
523
+ someSync((x) => x > 2, [1, 2, 3]); // true
524
+ everySync((x) => x > 0, [1, 2, 3]); // true
525
+ findSync((x) => x > 1, [1, 2, 3]); // 2
526
+ foldSync((a, b) => a + b, 0, [1, 2, 3, 4]); // 10
527
+ firstSync([1, 2, 3]); // 1
528
+ lastSync([1, 2, 3]); // 3
529
+ nthSync([10, 20, 30, 40], 2); // 30
530
+ quantifySync([1, 2, 3, 4], (x) => x % 2 === 0); // 2
531
+ minSync(["abc", "a", "ab"], (s) => s.length); // 'a'
532
+ maxSync(["abc", "a", "ab"], (s) => s.length); // 'abc'
533
+ ```
534
+
535
+ Need every item collected into an array instead? That's `exhaustSync`/
536
+ `exhaustAsync` (below) — no separate `toArraySync`/`toArrayAsync` alias is
537
+ provided.
538
+
539
+ ### Cancellation: `{ signal }` (new in 2.0)
540
+
541
+ Every async terminal consumer (`someAsync`, `everyAsync`, `findAsync`,
542
+ `forEachAsync`, `foldAsync`, `firstAsync`, `lastAsync`, `nthAsync`,
543
+ `quantifyAsync`, `minAsync`, `maxAsync`) accepts an optional trailing
544
+ `{ signal }` options bag, and the function returned by `transduceAsync`
545
+ accepts one as a second argument. On abort, the source iterator is closed
546
+ (`iterator.return()` is propagated upstream) and the operation rejects with
547
+ the signal's reason — an `"AbortError"` `DOMException` by default:
548
+
549
+ ```javascript
550
+ import { lastAsync, transduceAsync, transducers } from "@johnhenry/iteration";
551
+
552
+ const controller = new AbortController();
553
+ setTimeout(() => controller.abort(), 1000);
554
+
555
+ try {
556
+ await lastAsync(slowInfiniteStream(), undefined, { signal: controller.signal });
557
+ } catch (err) {
558
+ err.name; // 'AbortError'
559
+ }
560
+
561
+ // transduceAsync: pass { signal } when applying the pipeline to a source
562
+ const pipeline = transduceAsync(transducers.map((x) => x + 1));
563
+ for await (const item of pipeline(source, { signal: controller.signal })) {
564
+ // ...
565
+ }
566
+ ```
567
+
568
+ The underlying `abortable(iterable, signal)` async-generator wrapper is
569
+ exported too (also at `@johnhenry/iteration/abort`) if you want the same prompt,
570
+ `return()`-propagating cancellation around any `for await` loop.
571
+
572
+ ## Bounded concurrency (`@johnhenry/iteration/concurrency`, new in 2.0)
573
+
574
+ ### `mapConcurrentAsync`
575
+
576
+ `mapConcurrentAsync(fn, iterable, { concurrency, ordered = true, signal })`
577
+ maps `fn` over an (a)sync iterable with up to `concurrency` invocations in
578
+ flight at once. With `ordered: true` (default) results are yielded in input
579
+ order; with `ordered: false`, in completion order. The source is only
580
+ pulled while there is spare capacity (backpressure), `{ signal }` aborts
581
+ promptly even mid-`fn`, and early consumer exit / `fn` errors / abort all
582
+ close the source via `iterator.return()`.
583
+
584
+ ```javascript
585
+ import { mapConcurrentAsync } from "@johnhenry/iteration";
586
+ // or: import { mapConcurrentAsync } from "@johnhenry/iteration/concurrency";
587
+
588
+ for await (const page of mapConcurrentAsync(
589
+ (url) => fetch(url).then((r) => r.text()),
590
+ urls,
591
+ { concurrency: 4 }
592
+ )) {
593
+ console.log(page.length); // input order; pass ordered: false for completion order
594
+ }
595
+ ```
596
+
597
+ ### `prefetchAsync`
598
+
599
+ `prefetchAsync(n, iterable)` eagerly keeps up to `n` reads in flight ahead
600
+ of the consumer in a bounded buffer, overlapping a slow producer with a
601
+ slow consumer. `n <= 0` degenerates to plain iteration; early consumer exit
602
+ closes the source via `iterator.return()`.
603
+
604
+ ```javascript
605
+ import { prefetchAsync } from "@johnhenry/iteration";
606
+
607
+ for await (const record of prefetchAsync(8, slowDatabaseCursor())) {
608
+ await expensiveProcessing(record); // next 8 reads already in flight
609
+ }
610
+ ```
611
+
612
+ ## Utilities
613
+
614
+ This library provides a number of iterator related utilities.
615
+
616
+ ### `isIterator` & `isAsyncIterator` & `exhaustable`
617
+
618
+ Test of object is an iterator or asyncIterator, or either.
619
+
620
+ ```javascript
621
+ import { isIterator, isAsyncIterator, exhaustable } from "@johnhenry/iteration";
622
+ const iterator = (function* () {})();
623
+ const asyncIterator = (async function* () {})();
624
+ const block = {};
625
+ console.log(isIterator(iterator)); // true
626
+ console.log(isAsyncIterator(iterator)); // false
627
+ console.log(exhaustable(iterator)); // true
628
+ console.log(isIterator(asyncIterator)); // false
629
+ console.log(isAsyncIterator(asyncIterator)); // true
630
+ console.log(exhaustable(asyncIterator)); // true
631
+ console.log(isIterator(block)); // false
632
+ console.log(isAsyncIterator(block)); // false
633
+ console.log(exhaustable(block)); // false
634
+ ```
635
+
636
+ ### `exhaust` & `exhaustSync` & `exhaustAsync`
637
+
638
+ Exhaust all items from iterator — this library's "collect to an array"
639
+ (`toArray`) operation; see [Terminal consumers](#terminal-consumers-johnhenryiterationconsumers)
640
+ for other ways to resolve an iterable to a single value.
641
+ Warning: Initial object may have items removed
642
+
643
+ ```javascript
644
+ import { exhaust, exhaustSync, exhaustAsync } from "@johnhenry/iteration";
645
+ const iterator = [1, 2, 3];
646
+ const aIterator = (async function* () {
647
+ yield 4;
648
+ yield 5;
649
+ yield 6;
650
+ })();
651
+ const oneTwoThree = exhaust(iterator); //[1,2,3]
652
+ const fourFiveSix = await exhaust(iterator); //[4,5,6]
653
+ const oneTwoThreeB = exhaustSync(iterator); //[1,2,3]
654
+ const fourFiveSixB = await exhaustAsync(iterator); //[]
655
+ ```
656
+
657
+ ### `teeSync` & `teeAsync`
658
+
659
+ Tee iterator onto n other iterators
660
+ Warning: Initial object may be emptied
661
+
662
+ ```javascript
663
+ import { teeSync, teeAsync } from "@johnhenry/iteration";
664
+ const iterator = (async function* () {
665
+ yield 1;
666
+ yield 2;
667
+ yield 3;
668
+ })();
669
+
670
+ const [i0, i1, i2] = teeAsync(3)(iterator);
671
+
672
+ for (const x of i0) {
673
+ console.log(x);
674
+ } // logs 1, 2, 3
675
+
676
+ for (const x of transduce(map((x) => x - 1))(i1)) {
677
+ console.log(x);
678
+ } // logs 0, 1, 2
679
+
680
+ for (const x of transduce(filter((x) => x > 2))(i2)) {
681
+ console.log(x);
682
+ } // logs 3
683
+ ```
684
+
685
+ ### `AsyncChannel`
686
+
687
+ AsyncChannel is an experimental primative object.
688
+
689
+ **Backpressure (new in 2.0):** `put(item)` returns a `Promise<void>` that
690
+ resolves once the channel has accepted the item — immediately while a taker
691
+ is waiting or the cache is under `limit`, otherwise **when a later `take()`
692
+ frees a slot**. It no longer throws `"cache full"` at capacity. Blocked
693
+ producers release in FIFO order, `break()` queues behind already-waiting
694
+ producers (the end marker can't overtake their items), and `limit: 0`
695
+ behaves as a rendezvous channel (each `put` waits for its `take`).
696
+
697
+ ```javascript
698
+ const bounded = new AsyncChannel({ limit: 100 });
699
+ await bounded.put(item); // suspends the producer while the channel is full
700
+ ```
701
+
702
+ ```javascript
703
+ // file://declare.mjs
704
+ import { AsyncChannel } from "@johnhenry/iteration";
705
+ export const c = new AsyncChannel();
706
+ setTimeout(async () => {
707
+ for await (const i of c) {
708
+ console.log(i);
709
+ }
710
+ });
711
+ ```
712
+
713
+ Put items directly on an async channel.
714
+
715
+ ```javascript
716
+ // file://use.mjs
717
+ import { c } from "./declare.mjs";
718
+ c.put("hello"); //logs "hello"
719
+ c.put("world"); //logs "world"
720
+ ```
721
+
722
+ Automatically place items onto channels via decorators
723
+
724
+ ```javascript
725
+ // file://use-websocket.mjs
726
+ import { c } from "./declare.mjs";
727
+ import { withWebSocket } from "@johnhenry/iteration/channel-decorators";
728
+ const socket = new WebSocket(/*ws url*/);
729
+ withWebSocket(c, socket);
730
+ ```
731
+
732
+ ```javascript
733
+ // file://use-event-emitter.mjs
734
+ import { c } from "./declare.mjs";
735
+ import { withEmitter } from "@johnhenry/iteration/channel-decorators";
736
+ const source = new EventSource(/*sse url*/);
737
+ withEmitter(c, source);
738
+ ```
739
+
740
+ ## Releasing
741
+
742
+ Publishing is release-gated in GitHub Actions, matching the deployment setup in
743
+ [`johnhenry/math`](https://github.com/johnhenry/math):
744
+
745
+ 1. Bump `version` in `package.json` (and run `npm install --package-lock-only`
746
+ so the lockfile matches — `npm ci` fails if they diverge).
747
+ 2. Merge to `main`; CI runs typecheck, tests, build and the dist/`require(esm)`
748
+ smoke tests across Node 22.x and 24.x.
749
+ 3. Cut a **GitHub Release**. `.github/workflows/publish.yml` re-runs the full
750
+ verification, then publishes with npm provenance attestation.
751
+
752
+ The publish step is idempotent (`scripts/npm-publish-if-new.mjs` exits 0 when
753
+ the current version is already on the registry), so re-running a release or a
754
+ manual `workflow_dispatch` is safe.
755
+
756
+ Requires the **`NPM_TOKEN`** repository secret — a granular automation token
757
+ with write access to this package:
758
+
759
+ ```bash
760
+ gh secret set NPM_TOKEN --repo johnhenry/math
761
+ ```