@johnhenry/iteration 0.0.0 → 0.1.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 (50) hide show
  1. package/dist/async-channel.d.ts +24 -4
  2. package/dist/async-channel.d.ts.map +1 -1
  3. package/dist/async-channel.js +41 -38
  4. package/dist/async-channel.js.map +1 -1
  5. package/dist/combine.d.ts +146 -0
  6. package/dist/combine.d.ts.map +1 -0
  7. package/dist/combine.js +353 -0
  8. package/dist/combine.js.map +1 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/internal.d.ts +66 -0
  14. package/dist/internal.d.ts.map +1 -0
  15. package/dist/internal.js +134 -0
  16. package/dist/internal.js.map +1 -0
  17. package/dist/iterator-tools.d.ts +13 -0
  18. package/dist/iterator-tools.d.ts.map +1 -1
  19. package/dist/iterator-tools.js +38 -18
  20. package/dist/iterator-tools.js.map +1 -1
  21. package/dist/itertools.d.ts.map +1 -1
  22. package/dist/itertools.js +42 -24
  23. package/dist/itertools.js.map +1 -1
  24. package/dist/tee.d.ts +24 -2
  25. package/dist/tee.d.ts.map +1 -1
  26. package/dist/tee.js +175 -2
  27. package/dist/tee.js.map +1 -1
  28. package/dist/time.d.ts +69 -0
  29. package/dist/time.d.ts.map +1 -0
  30. package/dist/time.js +234 -0
  31. package/dist/time.js.map +1 -0
  32. package/dist/transduce.d.ts +65 -0
  33. package/dist/transduce.d.ts.map +1 -1
  34. package/dist/transduce.js +33 -1
  35. package/dist/transduce.js.map +1 -1
  36. package/dist/transducers.d.ts.map +1 -1
  37. package/dist/transducers.js +86 -16
  38. package/dist/transducers.js.map +1 -1
  39. package/package.json +10 -2
  40. package/readme.md +338 -14
  41. package/src/async-channel.ts +63 -47
  42. package/src/combine.ts +441 -0
  43. package/src/index.ts +2 -0
  44. package/src/internal.ts +164 -0
  45. package/src/iterator-tools.ts +48 -20
  46. package/src/itertools.ts +36 -23
  47. package/src/tee.ts +202 -2
  48. package/src/time.ts +262 -0
  49. package/src/transduce.ts +113 -1
  50. package/src/transducers.ts +121 -30
package/src/transduce.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { abortable, type SignalOptions } from "./abort.ts";
2
- import { type ReducerStep, reduceAsync, reduceSync, type Transducer } from "./iterator-tools.ts";
2
+ import { HALT, type ReducerStep, reduceAsync, reduceSync, type Transducer } from "./iterator-tools.ts";
3
3
 
4
4
  /**
5
5
  * The innermost reducer step ("emit"): pushes an emitted item onto the
@@ -159,3 +159,115 @@ export function transduceSync(
159
159
  export function transduceSync(...functions: Array<Transducer<any, any>>) {
160
160
  return (itemCollection: Iterable<unknown>) => reduceSync(itemCollection, composeFunctions(...functions)(emit), []);
161
161
  }
162
+
163
+ /**
164
+ * A push-driven transducer pipeline: feed values in one at a time with
165
+ * `step`, and flush with `complete` when the input ends.
166
+ */
167
+ export interface PushTransducer<In, Out> {
168
+ /**
169
+ * Run one value through the pipeline and return what it emitted (often
170
+ * zero or one item; `group` and friends may emit several). Returns `[]`
171
+ * once the pipeline has halted or completed.
172
+ */
173
+ step(value: In): Out[];
174
+ /**
175
+ * Flush stateful transducers (`.complete`, e.g. a trailing partial
176
+ * `group`) and return what they emitted. Call it once when the input
177
+ * ends, or after `halted` becomes true. Later calls return `[]`.
178
+ */
179
+ complete(): Out[];
180
+ /**
181
+ * What the stateful transducers keep between steps, upstream first: a
182
+ * half-full `group`'s items, `partitionBy`'s open run, `accumulate`'s
183
+ * running value, `dedupe`'s last key. A copy: changing it changes nothing.
184
+ */
185
+ held(): unknown[];
186
+ /**
187
+ * Let go of the held items `predicate` matches, and return how many. For a
188
+ * caller that must stop an operator keeping something (a value removed
189
+ * elsewhere). It never emits and never changes what is later emitted
190
+ * beyond leaving the dropped items out: a `group` still waits for its full
191
+ * size, a `partitionBy` run left empty is closed without being emitted,
192
+ * `accumulate` starts again from its initial value, and `dedupe` forgets
193
+ * its key (so an equal item passes once).
194
+ */
195
+ drop(predicate: (held: unknown) => boolean): number;
196
+ /** True once a transducer returned HALT (e.g. `take(n)` saw item n + 1). */
197
+ readonly halted: boolean;
198
+ }
199
+
200
+ /**
201
+ * Drive transducers by pushing values in, with no iterator: for code that
202
+ * receives values by callback (an event handler, a wire in a dataflow
203
+ * graph). Uses the same left-to-right composition order, `.complete` flush
204
+ * protocol and HALT semantics as transduceSync:
205
+ * `transduceSync(...t)(xs)` yields exactly what pushing each `x` through
206
+ * `step` and then calling `complete()` returns.
207
+ *
208
+ * Like transduceSync, the items emitted by the step that returns HALT are
209
+ * discarded.
210
+ * @kind function
211
+ * @name transducePush
212
+ * @example
213
+ * ```javascript
214
+ * import { transducePush, transducers } from '@johnhenry/iteration';
215
+ * const { map, group } = transducers;
216
+ * const pipe = transducePush(map((x) => x * 10), group(2));
217
+ * pipe.step(1); // []
218
+ * pipe.step(2); // [[10, 20]]
219
+ * pipe.step(3); // []
220
+ * pipe.complete(); // [[30]]
221
+ * ```
222
+ */
223
+ export function transducePush<A, B>(t1: Transducer<A, B>): PushTransducer<A, B>;
224
+ export function transducePush<A, B, C>(t1: Transducer<A, B>, t2: Transducer<B, C>): PushTransducer<A, C>;
225
+ export function transducePush<A, B, C, D>(
226
+ t1: Transducer<A, B>,
227
+ t2: Transducer<B, C>,
228
+ t3: Transducer<C, D>,
229
+ ): PushTransducer<A, D>;
230
+ export function transducePush<A, B, C, D, E>(
231
+ t1: Transducer<A, B>,
232
+ t2: Transducer<B, C>,
233
+ t3: Transducer<C, D>,
234
+ t4: Transducer<D, E>,
235
+ ): PushTransducer<A, E>;
236
+ export function transducePush<A, B, C, D, E, F>(
237
+ t1: Transducer<A, B>,
238
+ t2: Transducer<B, C>,
239
+ t3: Transducer<C, D>,
240
+ t4: Transducer<D, E>,
241
+ t5: Transducer<E, F>,
242
+ ): PushTransducer<A, F>;
243
+ export function transducePush(...functions: Array<Transducer<any, any>>): PushTransducer<unknown, unknown>;
244
+ export function transducePush(...functions: Array<Transducer<any, any>>): PushTransducer<unknown, unknown> {
245
+ const step = composeFunctions(...functions)(emit);
246
+ let halted = false;
247
+ let completed = false;
248
+ return {
249
+ step(value) {
250
+ if (halted || completed) return [];
251
+ const out = step([], value);
252
+ if (out === HALT) {
253
+ halted = true;
254
+ return [];
255
+ }
256
+ return out;
257
+ },
258
+ complete() {
259
+ if (completed) return [];
260
+ completed = true;
261
+ return step.complete ? step.complete([]) : [];
262
+ },
263
+ held() {
264
+ return step.held ? step.held() : [];
265
+ },
266
+ drop(predicate) {
267
+ return step.drop ? step.drop(predicate) : 0;
268
+ },
269
+ get halted() {
270
+ return halted;
271
+ },
272
+ };
273
+ }
@@ -8,6 +8,56 @@
8
8
 
9
9
  import { HALT, type ReducerStep, type Transducer } from "./iterator-tools.ts";
10
10
 
11
+ /**
12
+ * Give a stateless transducer's step the inner step's `.complete`, so the
13
+ * end-of-source flush cascades through it to a stateful transducer further
14
+ * down the pipeline (e.g. the trailing partial group in
15
+ * `transduceSync(map(f), group(2))`). A stateless step has nothing of its
16
+ * own to flush, so it forwards the inner `.complete` unchanged. Without
17
+ * this, `reduceSync`/`reduceAsync`/`transducePush` only ever saw the
18
+ * outermost step, which had no `.complete`, and the flush was lost.
19
+ * @ignore
20
+ */
21
+ const forwardComplete = <In>(
22
+ next: Pick<ReducerStep<never>, "complete" | "held" | "drop">,
23
+ step: ReducerStep<In>,
24
+ ): ReducerStep<In> => {
25
+ if (next.complete) step.complete = next.complete;
26
+ if (next.held) step.held = next.held;
27
+ if (next.drop) step.drop = next.drop;
28
+ return step;
29
+ };
30
+
31
+ /**
32
+ * Give a stateful step `held`/`drop` over its own state, then the inner
33
+ * step's, so the hooks cascade the way `.complete` does. `own` lists the
34
+ * step's held items; `ownDrop` lets go of the ones `predicate` matches and
35
+ * returns how many.
36
+ * @ignore
37
+ */
38
+ const holding = <In>(
39
+ next: Pick<ReducerStep<never>, "held" | "drop">,
40
+ step: ReducerStep<In>,
41
+ own: () => unknown[],
42
+ ownDrop: (predicate: (held: unknown) => boolean) => number,
43
+ ): ReducerStep<In> => {
44
+ step.held = () => [...own(), ...(next.held ? next.held() : [])];
45
+ step.drop = (predicate) => ownDrop(predicate) + (next.drop ? next.drop(predicate) : 0);
46
+ return step;
47
+ };
48
+
49
+ /** Remove, in place, the items of `list` that `predicate` matches; returns how many. */
50
+ const dropFrom = (list: unknown[], predicate: (held: unknown) => boolean): number => {
51
+ let kept = 0;
52
+ let dropped = 0;
53
+ for (const item of list) {
54
+ if (predicate(item)) dropped++;
55
+ else list[kept++] = item;
56
+ }
57
+ list.length = kept;
58
+ return dropped;
59
+ };
60
+
11
61
  /**
12
62
  * Create a transducer that maps values
13
63
  * @kind function
@@ -18,8 +68,7 @@ import { HALT, type ReducerStep, type Transducer } from "./iterator-tools.ts";
18
68
  export const map =
19
69
  <In, Out>(transform: (item: In) => Out): Transducer<In, Out> =>
20
70
  (conjoin) =>
21
- (init, item) =>
22
- conjoin(init, transform(item));
71
+ forwardComplete(conjoin, (init, item) => conjoin(init, transform(item)));
23
72
 
24
73
  /**
25
74
  * Create a transducer that filters values
@@ -31,8 +80,7 @@ export const map =
31
80
  export const filter =
32
81
  <In>(predicate: (item: In) => boolean): Transducer<In, In> =>
33
82
  (conjoin) =>
34
- (init, item) =>
35
- predicate(item) ? conjoin(init, item) : init;
83
+ forwardComplete(conjoin, (init, item) => (predicate(item) ? conjoin(init, item) : init));
36
84
 
37
85
  /**
38
86
  * Create a transducer that halts after a given number of values
@@ -46,7 +94,7 @@ export const take =
46
94
  (conjoin) => {
47
95
  let amount = 0;
48
96
  // biome-ignore lint/complexity/noCommaOperator: deliberate single-expression step function
49
- return (init, item) => (amount < limit ? (amount++, conjoin(init, item)) : HALT);
97
+ return forwardComplete(conjoin, (init, item) => (amount < limit ? (amount++, conjoin(init, item)) : HALT));
50
98
  };
51
99
 
52
100
  /**
@@ -62,8 +110,11 @@ export const drop =
62
110
  <In>(limit: number): Transducer<In, In> =>
63
111
  (conjoin) => {
64
112
  let amount = 0;
65
- // biome-ignore lint/complexity/noCommaOperator: deliberate single-expression step function
66
- return (init, item) => (amount >= limit ? (amount++, conjoin(init, item)) : (amount++, init));
113
+ return forwardComplete(conjoin, (init, item) => {
114
+ const pass = amount >= limit;
115
+ amount++;
116
+ return pass ? conjoin(init, item) : init;
117
+ });
67
118
  };
68
119
 
69
120
  /**
@@ -79,8 +130,7 @@ export const drop =
79
130
  export const reject =
80
131
  <In>(predicate: (item: In) => boolean): Transducer<In, In> =>
81
132
  (conjoin) =>
82
- (init, item) =>
83
- predicate(item) ? init : conjoin(init, item);
133
+ forwardComplete(conjoin, (init, item) => (predicate(item) ? init : conjoin(init, item)));
84
134
 
85
135
  /**
86
136
  * Create a transducer that groups items by quantity before emitting.
@@ -109,7 +159,12 @@ export const group =
109
159
  const buffer = out === HALT ? init : out;
110
160
  return conjoin.complete ? conjoin.complete(buffer) : buffer;
111
161
  };
112
- return step;
162
+ return holding(
163
+ conjoin,
164
+ step,
165
+ () => [...partition],
166
+ (predicate) => dropFrom(partition, predicate),
167
+ );
113
168
  };
114
169
 
115
170
  /**
@@ -134,10 +189,22 @@ export const accumulate =
134
189
  // directly here would carry accumulated state over into the *next*
135
190
  // transduce run that reuses this same accumulate(...) instance.
136
191
  let accumulated = initial;
137
- return (init, item) => {
138
- accumulated = func(accumulated, item);
139
- return conjoin(init, accumulated);
140
- };
192
+ let has = false;
193
+ return holding(
194
+ conjoin,
195
+ forwardComplete(conjoin, (init, item) => {
196
+ accumulated = func(accumulated, item);
197
+ has = true;
198
+ return conjoin(init, accumulated);
199
+ }),
200
+ () => (has ? [accumulated] : []),
201
+ (predicate) => {
202
+ if (!has || !predicate(accumulated)) return 0;
203
+ accumulated = initial;
204
+ has = false;
205
+ return 1;
206
+ },
207
+ );
141
208
  };
142
209
 
143
210
  /**
@@ -156,15 +223,25 @@ export const dedupe =
156
223
  (conjoin) => {
157
224
  let hasLast = false;
158
225
  let lastKey: unknown;
159
- return (init, item) => {
160
- const key = keyFn(item);
161
- if (hasLast && Object.is(key, lastKey)) {
162
- return init;
163
- }
164
- hasLast = true;
165
- lastKey = key;
166
- return conjoin(init, item);
167
- };
226
+ return holding(
227
+ conjoin,
228
+ forwardComplete(conjoin, (init, item) => {
229
+ const key = keyFn(item);
230
+ if (hasLast && Object.is(key, lastKey)) {
231
+ return init;
232
+ }
233
+ hasLast = true;
234
+ lastKey = key;
235
+ return conjoin(init, item);
236
+ }),
237
+ () => (hasLast ? [lastKey] : []),
238
+ (predicate) => {
239
+ if (!hasLast || !predicate(lastKey)) return 0;
240
+ hasLast = false;
241
+ lastKey = undefined;
242
+ return 1;
243
+ },
244
+ );
168
245
  };
169
246
 
170
247
  /**
@@ -179,7 +256,7 @@ export const interpose =
179
256
  <In, Sep = In>(separator: Sep): Transducer<In, In | Sep> =>
180
257
  (conjoin) => {
181
258
  let started = false;
182
- return (init, item) => {
259
+ return forwardComplete(conjoin, (init, item) => {
183
260
  if (started) {
184
261
  const out = conjoin(init, separator);
185
262
  if (out === HALT) {
@@ -189,7 +266,7 @@ export const interpose =
189
266
  }
190
267
  started = true;
191
268
  return conjoin(init, item);
192
- };
269
+ });
193
270
  };
194
271
 
195
272
  /**
@@ -235,7 +312,21 @@ export const partitionBy =
235
312
  const buffer = out === HALT ? init : out;
236
313
  return conjoin.complete ? conjoin.complete(buffer) : buffer;
237
314
  };
238
- return step;
315
+ return holding(
316
+ conjoin,
317
+ step,
318
+ () => (hasCurrent ? [...bucket] : []),
319
+ (predicate) => {
320
+ const dropped = dropFrom(bucket, predicate);
321
+ if (hasCurrent && bucket.length === 0) {
322
+ // A run with nothing left isn't open: the next item starts a fresh
323
+ // one, and complete() has no empty run to flush.
324
+ hasCurrent = false;
325
+ currentKey = undefined;
326
+ }
327
+ return dropped;
328
+ },
329
+ );
239
330
  };
240
331
 
241
332
  /**
@@ -250,7 +341,7 @@ export const partitionBy =
250
341
  export const tap =
251
342
  <In>(fn: (item: In) => unknown): Transducer<In, In> =>
252
343
  (conjoin) =>
253
- (init, item) => {
254
- fn(item);
255
- return conjoin(init, item);
256
- };
344
+ forwardComplete(conjoin, (init, item) => {
345
+ fn(item);
346
+ return conjoin(init, item);
347
+ });