@panagos/jojoscript 0.7.1 → 0.9.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.
@@ -1,327 +1,467 @@
1
- function iterable(value) {
2
- if (value == null) {
3
- throw new TypeError('Expected an iterable, got null or undefined')
4
- }
5
-
6
- if (typeof value[Symbol.iterator] === 'function') {
7
- return value
8
- }
9
-
10
- if (typeof value.next === 'function') {
11
- return {
12
- [Symbol.iterator]() {
13
- return value
14
- }
15
- }
16
- }
17
-
18
- throw new TypeError('Expected an iterable or iterator')
19
- }
20
-
21
- /*
22
- * Like `iterable`, but accepts (and prefers) an async iterable/iterator so
23
- * `for await` correctly awaits values produced by async generators such as
24
- * `mapAsync`/`filterAsync`, in addition to ordinary sync iterables.
25
- */
26
- function asyncIterable(value) {
27
- if (value == null) {
28
- throw new TypeError('Expected an iterable, got null or undefined')
29
- }
30
-
31
- if (
32
- typeof value[Symbol.asyncIterator] === 'function' ||
33
- typeof value[Symbol.iterator] === 'function'
34
- ) {
35
- return value
36
- }
37
-
38
- if (typeof value.next === 'function') {
39
- return {
40
- [Symbol.asyncIterator]() {
41
- return value
42
- }
43
- }
44
- }
45
-
46
- throw new TypeError('Expected an iterable or iterator')
47
- }
48
-
49
- export function map(items, fn) {
50
- return (function* () {
51
- let index = 0
52
- for (const item of iterable(items)) {
53
- yield fn(item, index++)
54
- }
55
- })()
56
- }
57
-
58
- export function filter(items, fn) {
59
- return (function* () {
60
- let index = 0
61
- for (const item of iterable(items)) {
62
- if (fn(item, index++)) yield item
63
- }
64
- })()
65
- }
66
-
67
- export function take(items, count) {
68
- return (function* () {
69
- if (count <= 0) return
70
-
71
- let emitted = 0
72
- for (const item of iterable(items)) {
73
- yield item
74
- emitted++
75
- if (emitted >= count) return
76
- }
77
- })()
78
- }
79
-
80
- export function skip(items, count) {
81
- return (function* () {
82
- let index = 0
83
- for (const item of iterable(items)) {
84
- if (index++ < count) continue
85
- yield item
86
- }
87
- })()
88
- }
89
-
90
- export function unique(items) {
91
- return (function* () {
92
- const seen = new Set()
93
- for (const item of iterable(items)) {
94
- if (seen.has(item)) continue
95
- seen.add(item)
96
- yield item
97
- }
98
- })()
99
- }
100
-
101
- export function flatMap(items, fn) {
102
- return (function* () {
103
- let index = 0
104
- for (const item of iterable(items)) {
105
- for (const value of iterable(fn(item, index++))) {
106
- yield value
107
- }
108
- }
109
- })()
110
- }
111
-
112
- export function reduce(items, fn, initial) {
113
- const iterator = iterable(items)[Symbol.iterator]()
114
- let accumulator
115
-
116
- if (arguments.length >= 3) {
117
- accumulator = initial
118
- } else {
119
- const first = iterator.next()
120
- if (first.done) {
121
- throw new TypeError('Reduce of empty iterable with no initial value')
122
- }
123
- accumulator = first.value
124
- }
125
-
126
- let index = arguments.length >= 3 ? 0 : 1
127
- for (let step = iterator.next(); !step.done; step = iterator.next()) {
128
- accumulator = fn(accumulator, step.value, index++)
129
- }
130
-
131
- return accumulator
132
- }
133
-
134
- export function toArray(items) {
135
- return Array.from(iterable(items))
136
- }
137
-
138
- export function count(items) {
139
- let result = 0
140
- for (const _ of iterable(items)) result++
141
- return result
142
- }
143
-
144
- export function first(items) {
145
- for (const item of iterable(items)) return item
146
- return undefined
147
- }
148
-
149
- export function find(items, predicate) {
150
- let index = 0
151
- for (const item of iterable(items)) {
152
- if (predicate(item, index++)) return item
153
- }
154
- return undefined
155
- }
156
-
157
- export function some(items, predicate) {
158
- let index = 0
159
- for (const item of iterable(items)) {
160
- if (predicate(item, index++)) return true
161
- }
162
- return false
163
- }
164
-
165
- export function every(items, predicate) {
166
- let index = 0
167
- for (const item of iterable(items)) {
168
- if (!predicate(item, index++)) return false
169
- }
170
- return true
171
- }
172
-
173
- export function sort(items, fn) {
174
- return Array.from(iterable(items)).sort(fn)
175
- }
176
-
177
- export function sortBy(items, keyFn) {
178
- return Array.from(iterable(items)).sort((a, b) => {
179
- const keyA = keyFn(a)
180
- const keyB = keyFn(b)
181
- if (keyA < keyB) return -1
182
- if (keyA > keyB) return 1
183
- return 0
184
- })
185
- }
186
-
187
- export function distinctBy(items, keyFn) {
188
- return (function* () {
189
- const seen = new Set()
190
- for (const item of iterable(items)) {
191
- const key = keyFn(item)
192
- if (seen.has(key)) continue
193
- seen.add(key)
194
- yield item
195
- }
196
- })()
197
- }
198
-
199
- export function tap(items, fn) {
200
- return (function* () {
201
- let index = 0
202
- for (const item of iterable(items)) {
203
- fn(item, index++)
204
- yield item
205
- }
206
- })()
207
- }
208
-
209
- export function chunk(items, size) {
210
- if (!Number.isInteger(size) || size <= 0) {
211
- throw new RangeError('chunk size must be a positive integer')
212
- }
213
-
214
- return (function* () {
215
- let buffer = []
216
- for (const item of iterable(items)) {
217
- buffer.push(item)
218
- if (buffer.length === size) {
219
- yield buffer
220
- buffer = []
221
- }
222
- }
223
- if (buffer.length) yield buffer
224
- })()
225
- }
226
-
227
- export function window(items, size) {
228
- if (!Number.isInteger(size) || size <= 0) {
229
- throw new RangeError('window size must be a positive integer')
230
- }
231
-
232
- return (function* () {
233
- let buffer = []
234
- for (const item of iterable(items)) {
235
- buffer.push(item)
236
- if (buffer.length > size) buffer.shift()
237
- if (buffer.length === size) yield buffer.slice()
238
- }
239
- })()
240
- }
241
-
242
- export function zip(items, other) {
243
- return (function* () {
244
- const left = iterable(items)[Symbol.iterator]()
245
- const right = iterable(other)[Symbol.iterator]()
246
-
247
- while (true) {
248
- const a = left.next()
249
- const b = right.next()
250
- if (a.done || b.done) return
251
- yield [a.value, b.value]
252
- }
253
- })()
254
- }
255
-
256
- export function zipWith(items, other, fn) {
257
- return (function* () {
258
- const left = iterable(items)[Symbol.iterator]()
259
- const right = iterable(other)[Symbol.iterator]()
260
-
261
- while (true) {
262
- const a = left.next()
263
- const b = right.next()
264
- if (a.done || b.done) return
265
- yield fn(a.value, b.value)
266
- }
267
- })()
268
- }
269
-
270
- export function scan(items, fn, initial) {
271
- return (function* () {
272
- let accumulator = initial
273
- let index = 0
274
- for (const item of iterable(items)) {
275
- accumulator = fn(accumulator, item, index++)
276
- yield accumulator
277
- }
278
- })()
279
- }
280
-
281
- export function partition(items, predicate) {
282
- const truthy = []
283
- const falsy = []
284
- let index = 0
285
-
286
- for (const item of iterable(items)) {
287
- if (predicate(item, index++)) {
288
- truthy.push(item)
289
- } else {
290
- falsy.push(item)
291
- }
292
- }
293
-
294
- return [truthy, falsy]
295
- }
296
-
297
- export function groupBy(items, keyFn) {
298
- const groups = new Map()
299
-
300
- for (const item of iterable(items)) {
301
- const key = keyFn(item)
302
- if (!groups.has(key)) groups.set(key, [])
303
- groups.get(key).push(item)
304
- }
305
-
306
- return groups
307
- }
308
-
309
- export async function* mapAsync(items, fn) {
310
- let index = 0
311
- for await (const item of asyncIterable(items)) {
312
- yield await fn(item, index++)
313
- }
314
- }
315
-
316
- export async function* filterAsync(items, fn) {
317
- let index = 0
318
- for await (const item of asyncIterable(items)) {
319
- if (await fn(item, index++)) yield item
320
- }
321
- }
322
-
323
- export async function toArrayAsync(items) {
324
- const result = []
325
- for await (const item of asyncIterable(items)) result.push(item)
326
- return result
327
- }
1
+ import { getParallelHandler } from './effects.js'
2
+
3
+ function iterable(value) {
4
+ if (value == null) {
5
+ throw new TypeError('Expected an iterable, got null or undefined')
6
+ }
7
+
8
+ if (typeof value[Symbol.iterator] === 'function') {
9
+ return value
10
+ }
11
+
12
+ /*
13
+ * An async generator/iterable (e.g. from `mapAsync`, `checkpoint`,
14
+ * `inspect`, or `traceNode` wrapping one of those) has `.next` too, but
15
+ * calling it synchronously here would silently loop forever comparing a
16
+ * Promise's `.done` (always `undefined`) instead of a real boolean. Fail
17
+ * fast with a clear message instead of hanging.
18
+ */
19
+ if (typeof value[Symbol.asyncIterator] === 'function') {
20
+ throw new TypeError(
21
+ 'Expected a sync iterable but got an async iterable/generator ' +
22
+ '(e.g. produced by mapAsync, filterAsync, checkpoint, inspect, or ' +
23
+ 'retry on an async function). Use the async equivalent instead ' +
24
+ '(e.g. toArrayAsync, or `for await`).'
25
+ )
26
+ }
27
+
28
+ if (typeof value.next === 'function') {
29
+ return {
30
+ [Symbol.iterator]() {
31
+ return value
32
+ }
33
+ }
34
+ }
35
+
36
+ throw new TypeError('Expected an iterable or iterator')
37
+ }
38
+
39
+ /*
40
+ * Like `iterable`, but accepts (and prefers) an async iterable/iterator so
41
+ * `for await` correctly awaits values produced by async generators such as
42
+ * `mapAsync`/`filterAsync`, in addition to ordinary sync iterables.
43
+ */
44
+ function asyncIterable(value) {
45
+ if (value == null) {
46
+ throw new TypeError('Expected an iterable, got null or undefined')
47
+ }
48
+
49
+ if (
50
+ typeof value[Symbol.asyncIterator] === 'function' ||
51
+ typeof value[Symbol.iterator] === 'function'
52
+ ) {
53
+ return value
54
+ }
55
+
56
+ if (typeof value.next === 'function') {
57
+ return {
58
+ [Symbol.asyncIterator]() {
59
+ return value
60
+ }
61
+ }
62
+ }
63
+
64
+ throw new TypeError('Expected an iterable or iterator')
65
+ }
66
+
67
+ const PARALLEL_TAG = Symbol('jojo.parallel')
68
+
69
+ /*
70
+ * Marks an iterable as wanting bounded-concurrency processing by the very
71
+ * next `map()` stage: `users |> parallel(8) |> map(fetchProfile)`. This
72
+ * does not itself run anything concurrently — it just tags the upstream
73
+ * iterable so `map()` knows to switch from sequential to a bounded worker
74
+ * pool. Any stage other than `map` receiving this tag is a usage error
75
+ * (the tag object has no iterator of its own).
76
+ */
77
+ export function parallel(items, concurrency, meta) {
78
+ if (!Number.isInteger(concurrency) || concurrency <= 0) {
79
+ throw new RangeError('parallel concurrency must be a positive integer')
80
+ }
81
+ return { [PARALLEL_TAG]: concurrency, source: items, meta }
82
+ }
83
+
84
+ function isParallelTagged(value) {
85
+ return value != null && typeof value === 'object' && PARALLEL_TAG in value
86
+ }
87
+
88
+ function getIterator(items) {
89
+ if (items == null) {
90
+ throw new TypeError('Expected an iterable, got null or undefined')
91
+ }
92
+ if (typeof items[Symbol.asyncIterator] === 'function') return items[Symbol.asyncIterator]()
93
+ if (typeof items[Symbol.iterator] === 'function') return items[Symbol.iterator]()
94
+ if (typeof items.next === 'function') return items
95
+ throw new TypeError('Expected an iterable or iterator')
96
+ }
97
+
98
+ /*
99
+ * Bounded-concurrency mapping: at most `concurrency` calls to `fn` are ever
100
+ * in flight at once (no unbounded `Promise.all`), memory usage stays
101
+ * bounded by `concurrency`, and results are yielded in the original order
102
+ * as soon as the item at the head of the window resolves — later items in
103
+ * the window keep running concurrently in the background while the head
104
+ * is awaited.
105
+ *
106
+ * `pullNext()` returns the in-flight promise wrapped in a plain object
107
+ * (`{ promise }`) rather than the promise itself: since `pullNext` is an
108
+ * `async function`, directly returning/awaiting the bare `fn(...)` promise
109
+ * would make JS "flatten" it — `await pullNext()` would then already be
110
+ * the *resolved value* of `fn`, silently serializing every call (no two
111
+ * calls ever truly overlap) and, since a resolved value of `0`/`''`/etc.
112
+ * is falsy, incorrectly being treated as "no more items".
113
+ */
114
+ async function* mapConcurrent(items, fn, concurrency) {
115
+ const iterator = getIterator(items)
116
+ const window = []
117
+ let index = 0
118
+ let exhausted = false
119
+
120
+ async function pullNext() {
121
+ if (exhausted) return undefined
122
+ const step = await iterator.next()
123
+ if (step.done) {
124
+ exhausted = true
125
+ return undefined
126
+ }
127
+ const i = index++
128
+ return { promise: Promise.resolve(fn(step.value, i)) }
129
+ }
130
+
131
+ for (let i = 0; i < concurrency && !exhausted; i++) {
132
+ const started = await pullNext()
133
+ if (started) window.push(started)
134
+ }
135
+
136
+ while (window.length) {
137
+ const head = window.shift()
138
+ yield await head.promise
139
+ if (!exhausted) {
140
+ const started = await pullNext()
141
+ if (started) window.push(started)
142
+ }
143
+ }
144
+ }
145
+
146
+ /*
147
+ * A `PARALLEL` handler may be an ordinary sync function returning an
148
+ * array, an async function returning a Promise of an (async) iterable, or
149
+ * an async generator function returning an async iterable directly (no
150
+ * Promise wrapper at all). This normalizes all three into a single async
151
+ * iterable so `map()`'s return contract stays the same either way.
152
+ */
153
+ async function* toAsyncIterable(resultOrPromise) {
154
+ const result = await resultOrPromise
155
+ for await (const item of result) yield item
156
+ }
157
+
158
+ export function map(items, fn) {
159
+ if (isParallelTagged(items)) {
160
+ const concurrency = items[PARALLEL_TAG]
161
+ const handler = getParallelHandler()
162
+
163
+ /*
164
+ * `parallel(n)` is effect-backed: a `PARALLEL` handler installed via
165
+ * `handle { PARALLEL: ... } { ... }` decides how this actually runs
166
+ * (real concurrency, deterministic sequential execution, recorded
167
+ * scheduling, ...) instead of the default bounded-concurrency worker
168
+ * pool below. With no handler installed — the common case — behavior
169
+ * is exactly what it was before effects existed.
170
+ */
171
+ if (handler) {
172
+ return toAsyncIterable(handler({ items: items.source, fn, concurrency, meta: items.meta }))
173
+ }
174
+
175
+ return mapConcurrent(items.source, fn, concurrency)
176
+ }
177
+ return (function* () {
178
+ let index = 0
179
+ for (const item of iterable(items)) {
180
+ yield fn(item, index++)
181
+ }
182
+ })()
183
+ }
184
+
185
+ export function filter(items, fn) {
186
+ return (function* () {
187
+ let index = 0
188
+ for (const item of iterable(items)) {
189
+ if (fn(item, index++)) yield item
190
+ }
191
+ })()
192
+ }
193
+
194
+ export function take(items, count) {
195
+ return (function* () {
196
+ if (count <= 0) return
197
+
198
+ let emitted = 0
199
+ for (const item of iterable(items)) {
200
+ yield item
201
+ emitted++
202
+ if (emitted >= count) return
203
+ }
204
+ })()
205
+ }
206
+
207
+ export function skip(items, count) {
208
+ return (function* () {
209
+ let index = 0
210
+ for (const item of iterable(items)) {
211
+ if (index++ < count) continue
212
+ yield item
213
+ }
214
+ })()
215
+ }
216
+
217
+ export function unique(items) {
218
+ return (function* () {
219
+ const seen = new Set()
220
+ for (const item of iterable(items)) {
221
+ if (seen.has(item)) continue
222
+ seen.add(item)
223
+ yield item
224
+ }
225
+ })()
226
+ }
227
+
228
+ export function flatMap(items, fn) {
229
+ return (function* () {
230
+ let index = 0
231
+ for (const item of iterable(items)) {
232
+ for (const value of iterable(fn(item, index++))) {
233
+ yield value
234
+ }
235
+ }
236
+ })()
237
+ }
238
+
239
+ export function reduce(items, fn, initial) {
240
+ const iterator = iterable(items)[Symbol.iterator]()
241
+ let accumulator
242
+
243
+ if (arguments.length >= 3) {
244
+ accumulator = initial
245
+ } else {
246
+ const first = iterator.next()
247
+ if (first.done) {
248
+ throw new TypeError('Reduce of empty iterable with no initial value')
249
+ }
250
+ accumulator = first.value
251
+ }
252
+
253
+ let index = arguments.length >= 3 ? 0 : 1
254
+ for (let step = iterator.next(); !step.done; step = iterator.next()) {
255
+ accumulator = fn(accumulator, step.value, index++)
256
+ }
257
+
258
+ return accumulator
259
+ }
260
+
261
+ export function toArray(items) {
262
+ return Array.from(iterable(items))
263
+ }
264
+
265
+ export function count(items) {
266
+ let result = 0
267
+ for (const _ of iterable(items)) result++
268
+ return result
269
+ }
270
+
271
+ export function first(items) {
272
+ for (const item of iterable(items)) return item
273
+ return undefined
274
+ }
275
+
276
+ export function find(items, predicate) {
277
+ let index = 0
278
+ for (const item of iterable(items)) {
279
+ if (predicate(item, index++)) return item
280
+ }
281
+ return undefined
282
+ }
283
+
284
+ export function some(items, predicate) {
285
+ let index = 0
286
+ for (const item of iterable(items)) {
287
+ if (predicate(item, index++)) return true
288
+ }
289
+ return false
290
+ }
291
+
292
+ export function every(items, predicate) {
293
+ let index = 0
294
+ for (const item of iterable(items)) {
295
+ if (!predicate(item, index++)) return false
296
+ }
297
+ return true
298
+ }
299
+
300
+ export function sort(items, fn) {
301
+ return Array.from(iterable(items)).sort(fn)
302
+ }
303
+
304
+ export function sortBy(items, keyFn) {
305
+ return Array.from(iterable(items)).sort((a, b) => {
306
+ const keyA = keyFn(a)
307
+ const keyB = keyFn(b)
308
+ if (keyA < keyB) return -1
309
+ if (keyA > keyB) return 1
310
+ return 0
311
+ })
312
+ }
313
+
314
+ export function distinctBy(items, keyFn) {
315
+ return (function* () {
316
+ const seen = new Set()
317
+ for (const item of iterable(items)) {
318
+ const key = keyFn(item)
319
+ if (seen.has(key)) continue
320
+ seen.add(key)
321
+ yield item
322
+ }
323
+ })()
324
+ }
325
+
326
+ export function tap(items, fn) {
327
+ return (function* () {
328
+ let index = 0
329
+ for (const item of iterable(items)) {
330
+ fn(item, index++)
331
+ yield item
332
+ }
333
+ })()
334
+ }
335
+
336
+ export function chunk(items, size) {
337
+ if (!Number.isInteger(size) || size <= 0) {
338
+ throw new RangeError('chunk size must be a positive integer')
339
+ }
340
+
341
+ return (function* () {
342
+ let buffer = []
343
+ for (const item of iterable(items)) {
344
+ buffer.push(item)
345
+ if (buffer.length === size) {
346
+ yield buffer
347
+ buffer = []
348
+ }
349
+ }
350
+ if (buffer.length) yield buffer
351
+ })()
352
+ }
353
+
354
+ /*
355
+ * Groups a lazy/streaming source into fixed-size arrays, remaining lazy —
356
+ * items are never buffered beyond the current, not-yet-full batch, which
357
+ * is exactly the bounded buffering §13 (backpressure) requires. Equivalent
358
+ * to `chunk`, exposed under the pipeline's own vocabulary.
359
+ */
360
+ export function batch(items, size, meta) {
361
+ if (!Number.isInteger(size) || size <= 0) {
362
+ throw new RangeError('batch size must be a positive integer')
363
+ }
364
+ return chunk(items, size)
365
+ }
366
+
367
+ export function window(items, size) {
368
+ if (!Number.isInteger(size) || size <= 0) {
369
+ throw new RangeError('window size must be a positive integer')
370
+ }
371
+
372
+ return (function* () {
373
+ let buffer = []
374
+ for (const item of iterable(items)) {
375
+ buffer.push(item)
376
+ if (buffer.length > size) buffer.shift()
377
+ if (buffer.length === size) yield buffer.slice()
378
+ }
379
+ })()
380
+ }
381
+
382
+ export function zip(items, other) {
383
+ return (function* () {
384
+ const left = iterable(items)[Symbol.iterator]()
385
+ const right = iterable(other)[Symbol.iterator]()
386
+
387
+ while (true) {
388
+ const a = left.next()
389
+ const b = right.next()
390
+ if (a.done || b.done) return
391
+ yield [a.value, b.value]
392
+ }
393
+ })()
394
+ }
395
+
396
+ export function zipWith(items, other, fn) {
397
+ return (function* () {
398
+ const left = iterable(items)[Symbol.iterator]()
399
+ const right = iterable(other)[Symbol.iterator]()
400
+
401
+ while (true) {
402
+ const a = left.next()
403
+ const b = right.next()
404
+ if (a.done || b.done) return
405
+ yield fn(a.value, b.value)
406
+ }
407
+ })()
408
+ }
409
+
410
+ export function scan(items, fn, initial) {
411
+ return (function* () {
412
+ let accumulator = initial
413
+ let index = 0
414
+ for (const item of iterable(items)) {
415
+ accumulator = fn(accumulator, item, index++)
416
+ yield accumulator
417
+ }
418
+ })()
419
+ }
420
+
421
+ export function partition(items, predicate) {
422
+ const truthy = []
423
+ const falsy = []
424
+ let index = 0
425
+
426
+ for (const item of iterable(items)) {
427
+ if (predicate(item, index++)) {
428
+ truthy.push(item)
429
+ } else {
430
+ falsy.push(item)
431
+ }
432
+ }
433
+
434
+ return [truthy, falsy]
435
+ }
436
+
437
+ export function groupBy(items, keyFn) {
438
+ const groups = new Map()
439
+
440
+ for (const item of iterable(items)) {
441
+ const key = keyFn(item)
442
+ if (!groups.has(key)) groups.set(key, [])
443
+ groups.get(key).push(item)
444
+ }
445
+
446
+ return groups
447
+ }
448
+
449
+ export async function* mapAsync(items, fn) {
450
+ let index = 0
451
+ for await (const item of asyncIterable(items)) {
452
+ yield await fn(item, index++)
453
+ }
454
+ }
455
+
456
+ export async function* filterAsync(items, fn) {
457
+ let index = 0
458
+ for await (const item of asyncIterable(items)) {
459
+ if (await fn(item, index++)) yield item
460
+ }
461
+ }
462
+
463
+ export async function toArrayAsync(items) {
464
+ const result = []
465
+ for await (const item of asyncIterable(items)) result.push(item)
466
+ return result
467
+ }