@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.
- package/MIT-LICENSE.txt +20 -0
- package/dist/abort.d.ts +26 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +69 -0
- package/dist/abort.js.map +1 -0
- package/dist/assertions/eventualequal.d.ts +9 -0
- package/dist/assertions/eventualequal.d.ts.map +1 -0
- package/dist/assertions/eventualequal.js +30 -0
- package/dist/assertions/eventualequal.js.map +1 -0
- package/dist/async-channel.d.ts +101 -0
- package/dist/async-channel.d.ts.map +1 -0
- package/dist/async-channel.js +208 -0
- package/dist/async-channel.js.map +1 -0
- package/dist/channel-decorators.d.ts +23 -0
- package/dist/channel-decorators.d.ts.map +1 -0
- package/dist/channel-decorators.js +31 -0
- package/dist/channel-decorators.js.map +1 -0
- package/dist/concurrency.d.ts +43 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +145 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/consumers.d.ts +199 -0
- package/dist/consumers.d.ts.map +1 -0
- package/dist/consumers.js +349 -0
- package/dist/consumers.js.map +1 -0
- package/dist/count.d.ts +37 -0
- package/dist/count.d.ts.map +1 -0
- package/dist/count.js +70 -0
- package/dist/count.js.map +1 -0
- package/dist/empty-iterator.d.ts +15 -0
- package/dist/empty-iterator.d.ts.map +1 -0
- package/dist/empty-iterator.js +16 -0
- package/dist/empty-iterator.js.map +1 -0
- package/dist/eventualequal.d.ts +2 -0
- package/dist/eventualequal.d.ts.map +1 -0
- package/dist/eventualequal.js +54 -0
- package/dist/eventualequal.js.map +1 -0
- package/dist/exhaust.d.ts +7 -0
- package/dist/exhaust.d.ts.map +1 -0
- package/dist/exhaust.js +22 -0
- package/dist/exhaust.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/is-iterator.d.ts +5 -0
- package/dist/is-iterator.d.ts.map +1 -0
- package/dist/is-iterator.js +5 -0
- package/dist/is-iterator.js.map +1 -0
- package/dist/iterator-tools.d.ts +164 -0
- package/dist/iterator-tools.d.ts.map +1 -0
- package/dist/iterator-tools.js +231 -0
- package/dist/iterator-tools.js.map +1 -0
- package/dist/itertools.d.ts +237 -0
- package/dist/itertools.d.ts.map +1 -0
- package/dist/itertools.js +471 -0
- package/dist/itertools.js.map +1 -0
- package/dist/tee.d.ts +18 -0
- package/dist/tee.d.ts.map +1 -0
- package/dist/tee.js +118 -0
- package/dist/tee.js.map +1 -0
- package/dist/transduce.d.ts +79 -0
- package/dist/transduce.d.ts.map +1 -0
- package/dist/transduce.js +37 -0
- package/dist/transduce.js.map +1 -0
- package/dist/transducers.d.ts +123 -0
- package/dist/transducers.d.ts.map +1 -0
- package/dist/transducers.js +212 -0
- package/dist/transducers.js.map +1 -0
- package/package.json +122 -0
- package/readme.md +761 -0
- package/src/abort.ts +80 -0
- package/src/assertions/eventualequal.ts +36 -0
- package/src/async-channel.ts +228 -0
- package/src/channel-decorators.ts +49 -0
- package/src/concurrency.ts +168 -0
- package/src/consumers.ts +425 -0
- package/src/count.ts +72 -0
- package/src/empty-iterator.ts +17 -0
- package/src/eventualequal.ts +54 -0
- package/src/exhaust.ts +26 -0
- package/src/index.ts +17 -0
- package/src/is-iterator.ts +9 -0
- package/src/iterator-tools.ts +290 -0
- package/src/itertools.ts +518 -0
- package/src/tee.ts +136 -0
- package/src/transduce.ts +161 -0
- 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
|
+
```
|