@jarenjs/linq 0.34.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.
@@ -0,0 +1,543 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The deferred, immutable `Sequence`. Every operator returns a
4
+ * NEW sequence; nothing runs until a terminal operation; a sequence may
5
+ * be enumerated repeatedly and each enumeration re-reads its source —
6
+ * the C# contract, including the part that surprises people
7
+ * (LINQ-FORMAT.md §5 has the worked example).
8
+ *
9
+ * The chain is data: `toDocument()` emits one Jaren query document, and
10
+ * a terminal either compiles it in memory (the reference semantics) or
11
+ * hands it WHOLE to a provider (`execute(document, options)`, D2) —
12
+ * which is what makes a query loggable, storable, diffable and
13
+ * authorable by a constrained decoder.
14
+ */
15
+
16
+ import { captureExpression, toExpression } from './expression.js';
17
+ import { emitDocument, wrapTerminal } from './document.js';
18
+ import { classifySource, compileDocument, executeInMemory } from './provider.js';
19
+ import { asyncFromSequence } from './async.js';
20
+ import { LinqBuildError, LinqRuntimeError } from './errors.js';
21
+
22
+ /** Binding names the emitted documents own; parameters may not shadow
23
+ * them (LINQ-FORMAT.md §7). */
24
+ const RESERVED_NAMES = new Set(['it', 'it2', 'acc', 'g']);
25
+
26
+ const VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
27
+
28
+ /**
29
+ * A deep, independent copy of an emitted query document. Plain data
30
+ * only, which is exactly what a document is — every captured expression
31
+ * has already passed the JSON-domain boundary in `expression.js`, so
32
+ * there is nothing here a structural copy would lose.
33
+ * @param {any} node
34
+ * @returns {any}
35
+ */
36
+ function snapshot(node) {
37
+ if (node === null || typeof node !== 'object') return node;
38
+ if (Array.isArray(node)) return node.map(snapshot);
39
+ /** @type {Record<string, any>} */
40
+ const out = {};
41
+ for (const key of Object.keys(node)) defineOwn(out, key, snapshot(node[key]));
42
+ return out;
43
+ }
44
+
45
+ /**
46
+ * Assign an OWN property, so a `__proto__` member stays a member instead
47
+ * of silently replacing the object's prototype and vanishing.
48
+ * @param {Record<string, any>} target
49
+ * @param {string} key
50
+ * @param {any} value
51
+ */
52
+ function defineOwn(target, key, value) {
53
+ Object.defineProperty(target, key,
54
+ { value, writable: true, enumerable: true, configurable: true });
55
+ }
56
+
57
+ /** @param {number} value @param {string} what */
58
+ function requireIndex(value, what) {
59
+ if (!Number.isInteger(value) || value < 0) {
60
+ throw new LinqBuildError('JL0005', `${what} takes a non-negative integer, got ${value}`);
61
+ }
62
+ return value;
63
+ }
64
+
65
+ /** The immutable query sequence. Construct via `from`/`fromDocument`. */
66
+ export class Sequence {
67
+ #source;
68
+ #sourceKind;
69
+ #root;
70
+ #stages;
71
+ #params;
72
+ #options;
73
+
74
+ /**
75
+ * @param {any} source
76
+ * @param {'provider' | 'iterable'} sourceKind
77
+ * @param {any} root - the root expression items come from
78
+ * @param {readonly any[]} stages
79
+ * @param {ReadonlyMap<string, any>} params
80
+ * @param {{ compileTypeTest?: any, functions?: any, collations?: any,
81
+ * pathFunctions?: any, limits?: any, registry?: object }} options
82
+ */
83
+ constructor(source, sourceKind, root, stages, params, options) {
84
+ this.#source = source;
85
+ this.#sourceKind = sourceKind;
86
+ this.#root = root;
87
+ this.#stages = stages;
88
+ this.#params = params;
89
+ this.#options = options;
90
+ }
91
+
92
+ /** @param {any} stage */
93
+ #with(stage) {
94
+ return new Sequence(this.#source, this.#sourceKind, this.#root,
95
+ [...this.#stages, stage], this.#params, this.#options);
96
+ }
97
+
98
+ #declared() {
99
+ return new Set(this.#params.keys());
100
+ }
101
+
102
+ /** @param {(...roots: any[]) => any} fn @param {readonly any[]} roots */
103
+ #capture(fn, roots = ['it']) {
104
+ if (typeof fn !== 'function') {
105
+ throw new LinqBuildError('JL0005', 'this operator takes a callback function');
106
+ }
107
+ return captureExpression(fn, roots, this.#declared());
108
+ }
109
+
110
+ //#region operators (each returns a new immutable Sequence)
111
+
112
+ /** Filter: `.where(it => it.age.gt(21))` → FLWOR `$where`. */
113
+ where(predicate) {
114
+ return this.#with({ kind: 'where', predicate: this.#capture(predicate) });
115
+ }
116
+
117
+ /** Project: `.select(it => ({ id: it.id }))` → `$return`. */
118
+ select(projection) {
119
+ return this.#with({ kind: 'select', projection: this.#capture(projection) });
120
+ }
121
+
122
+ /** Project-and-flatten: a multi-item projection concatenates (the
123
+ * FLWOR `$return` already flattens per tuple). */
124
+ selectMany(selector) {
125
+ return this.#with({ kind: 'select', projection: this.#capture(selector) });
126
+ }
127
+
128
+ /** @param {any} key @param {boolean} desc @param {any} [options] */
129
+ #orderStage(kind, key, desc, options) {
130
+ const spec = { $key: this.#capture(key) };
131
+ if (desc) spec.$dir = 'desc';
132
+ if (options !== undefined) {
133
+ if (options.empty !== undefined) spec.$empty = options.empty;
134
+ if (options.collation !== undefined) spec.$collation = options.collation;
135
+ }
136
+ return this.#with({ kind, spec });
137
+ }
138
+
139
+ /** Sort ascending → an `$orderby` key spec (`$dir`/`$empty`/
140
+ * `$collation` exposed through `options`).
141
+ * @param {(...roots: any[]) => any} key
142
+ * @param {{ empty?: string, collation?: string }} [options] */
143
+ orderBy(key, options) {
144
+ return this.#orderStage('orderBy', key, false, options);
145
+ }
146
+
147
+ /** @param {(...roots: any[]) => any} key
148
+ * @param {{ empty?: string, collation?: string }} [options] */
149
+ orderByDescending(key, options) {
150
+ return this.#orderStage('orderBy', key, true, options);
151
+ }
152
+
153
+ /** Secondary sort key; must directly follow `orderBy*` (JL0005).
154
+ * @param {(...roots: any[]) => any} key
155
+ * @param {{ empty?: string, collation?: string }} [options] */
156
+ thenBy(key, options) {
157
+ return this.#orderStage('thenBy', key, false, options);
158
+ }
159
+
160
+ /** @param {(...roots: any[]) => any} key
161
+ * @param {{ empty?: string, collation?: string }} [options] */
162
+ thenByDescending(key, options) {
163
+ return this.#orderStage('thenBy', key, true, options);
164
+ }
165
+
166
+ /** Group → `$groupby`; downstream items are `{ key, items }`. */
167
+ groupBy(key) {
168
+ return this.#with({ kind: 'groupBy', key: this.#capture(key) });
169
+ }
170
+
171
+ /** Both sides read ONE input document in 0.1 — a query document has
172
+ * one root. Cross-source composition arrives with the relational order.
173
+ * @param {Sequence} inner @param {string} what */
174
+ #requireSameSource(inner, what) {
175
+ if (!(inner instanceof Sequence)) {
176
+ throw new LinqBuildError('JL0005', `${what} takes another sequence as its inner side`);
177
+ }
178
+ if (inner.#source !== this.#source) {
179
+ throw new LinqBuildError('JL0005',
180
+ `${what}'s other side must derive from the same source in 0.1 — `
181
+ + 'a query document reads one input; load both collections under one root '
182
+ + '(the relational order lifts this)');
183
+ }
184
+ }
185
+
186
+ /** Equi-join → nested `$for` + `$where` equality (the engine rewrites
187
+ * this shape to a hash join; that is why it is fast). */
188
+ join(inner, outerKey, innerKey, result) {
189
+ this.#requireSameSource(inner, 'join');
190
+ return this.#with({
191
+ kind: 'join',
192
+ inner: inner.toDocument(),
193
+ on: {
194
+ $eq: [this.#capture(outerKey), this.#capture(innerKey, ['it2'])],
195
+ },
196
+ result: this.#capture(result, ['it', 'it2']),
197
+ });
198
+ }
199
+
200
+ /** Group-join: the result selector receives the outer item and the
201
+ * MATCHING inner group as an expression (`(u, g) => ({ n: g.count() })`). */
202
+ groupJoin(inner, outerKey, innerKey, result) {
203
+ this.#requireSameSource(inner, 'groupJoin');
204
+ const group = {
205
+ $for: { it2: inner.toDocument() },
206
+ $where: { $eq: [this.#capture(outerKey), this.#capture(innerKey, ['it2'])] },
207
+ $return: '$it2',
208
+ };
209
+ return this.#with({
210
+ kind: 'select',
211
+ projection: this.#capture(result, ['it', { doc: group, pathable: false }]),
212
+ });
213
+ }
214
+
215
+ /** Seeded fold → `$fold` (the accumulator clause). Only the seeded
216
+ * form exists: JSON has no way to spell an unseeded lambda's implicit
217
+ * first element without one. */
218
+ aggregate(seed, step) {
219
+ return this.#with({
220
+ kind: 'aggregate',
221
+ seed: toExpression(seed),
222
+ step: this.#capture(step, ['acc', 'it']),
223
+ });
224
+ }
225
+
226
+ skip(count) {
227
+ return this.#with({ kind: 'skip', count: requireIndex(count, 'skip') });
228
+ }
229
+
230
+ take(count) {
231
+ return this.#with({ kind: 'take', count: requireIndex(count, 'take') });
232
+ }
233
+
234
+ distinct() {
235
+ return this.#with({ kind: 'distinct' });
236
+ }
237
+
238
+ reverse() {
239
+ return this.#with({ kind: 'reverse' });
240
+ }
241
+
242
+ /** Concatenate another sequence over the SAME source, or a constant
243
+ * array (embedded verbatim; its elements join the stream).
244
+ *
245
+ * The same-source check is not a formality. A query document reads ONE
246
+ * input, so the other sequence contributes its expression, not its
247
+ * data — and a sequence built over a different source would have its
248
+ * expression evaluated against THIS source, quietly reading the wrong
249
+ * rows twice instead of concatenating two inputs. */
250
+ concat(other) {
251
+ let expr;
252
+ if (other instanceof Sequence) {
253
+ this.#requireSameSource(other, 'concat');
254
+ expr = other.toDocument();
255
+ }
256
+ else if (Array.isArray(other)) {
257
+ expr = { $for: { it: { $const: other } }, $return: '$it' };
258
+ }
259
+ else {
260
+ throw new LinqBuildError('JL0005', 'concat takes a sequence or a constant array');
261
+ }
262
+ return this.#with({ kind: 'concat', other: expr });
263
+ }
264
+
265
+ /** `$default`: the sequence, or the fallback when it is empty. */
266
+ defaultIfEmpty(fallback = null) {
267
+ return this.#with({ kind: 'defaultIfEmpty', fallback: toExpression(fallback) });
268
+ }
269
+
270
+ /** Keep only items matching the JSON Schema (`$valid` filter). */
271
+ ofType(schema) {
272
+ return this.#with({ kind: 'ofType', schema });
273
+ }
274
+
275
+ /** Assert every item against the JSON Schema (`$assert`). */
276
+ cast(schema) {
277
+ return this.#with({ kind: 'cast', schema });
278
+ }
279
+
280
+ /** Cross into the async surface: everything BEFORE this call is the
281
+ * prefix — compiled in memory, or pushed WHOLE to the provider — and
282
+ * `mapAsync` plus everything after runs locally over its rows.
283
+ * `explain()` on the result reports the split (LINQ-FORMAT.md §11).
284
+ * @param {(item: any, signal: AbortSignal) => any} fn
285
+ * @param {{ concurrency: number, mode?: string, ordered?: boolean }} options */
286
+ mapAsync(fn, options) {
287
+ return asyncFromSequence({
288
+ runPrefix: () => this.toArray(),
289
+ prefixDocument: () => this.toDocument(),
290
+ params: this.#params,
291
+ options: this.#options,
292
+ }, fn, options);
293
+ }
294
+
295
+ /** Recorded `unsupported` (LINQ-FORMAT.md §4): the grammar has no
296
+ * positional co-iteration. */
297
+ zip() {
298
+ throw new LinqBuildError('JL0006',
299
+ 'zip is unsupported: the query grammar has no positional co-iteration (see LINQ-FORMAT.md §4)');
300
+ }
301
+
302
+ /** Declare (and bind) external parameters: `.params({ tenantId })`.
303
+ * Callbacks read them through their last argument (`(it, p) =>
304
+ * it.tenant.eq(p.tenantId)`); the emitted document carries them as
305
+ * externals — the seam that becomes bound SQL parameters. */
306
+ params(bindings) {
307
+ if (bindings === null || typeof bindings !== 'object' || Array.isArray(bindings)) {
308
+ throw new LinqBuildError('JL0004', 'params takes an object of name → value bindings');
309
+ }
310
+ const merged = new Map(this.#params);
311
+ for (const name of Object.keys(bindings)) {
312
+ if (!VAR_NAME_RE.test(name)) {
313
+ throw new LinqBuildError('JL0004', `'${name}' is not a valid parameter name`);
314
+ }
315
+ if (RESERVED_NAMES.has(name)) {
316
+ throw new LinqBuildError('JL0004',
317
+ `'${name}' is reserved (the emitted document's own binding names: it, it2, acc, g)`);
318
+ }
319
+ merged.set(name, bindings[name]);
320
+ }
321
+ return new Sequence(this.#source, this.#sourceKind, this.#root,
322
+ this.#stages, merged, this.#options);
323
+ }
324
+
325
+ //#endregion
326
+
327
+ //#region documents and execution
328
+
329
+ /** The chain as ONE query document — public API, not a debug toy:
330
+ * loggable, cacheable, storable, diffable, transportable, and
331
+ * compilable by a bare `compileJsonQuery` with no linq involvement.
332
+ *
333
+ * A DEEP, independent snapshot. The emitted tree embeds the captured
334
+ * expressions a stage holds, so handing them out by reference made this
335
+ * a live window into a sequence documented as immutable: writing into
336
+ * the returned document rewrote the predicate, and the next
337
+ * enumeration answered differently. A snapshot cannot do that. */
338
+ toDocument() {
339
+ return snapshot(emitDocument(this.#root, this.#stages));
340
+ }
341
+
342
+ /** The compiled view of the chain: the document, its externals and
343
+ * its dependency sets.
344
+ *
345
+ * This always explains the IN-MEMORY compilation — it is the reference
346
+ * semantics, and it is not the provider's plan. It cannot report SQL
347
+ * pushdown, index use, residual execution or a strict refusal, and it
348
+ * will fail on an operator or collation only the provider can compile.
349
+ * For a provider's real plan, emit `toDocument()` and call that
350
+ * provider's own explanation. */
351
+ explain() {
352
+ const document = this.toDocument();
353
+ const compiled = compileDocument(document, {
354
+ ...this.#options,
355
+ externals: [...this.#params.keys()],
356
+ });
357
+ return {
358
+ document,
359
+ externals: [...compiled.externals],
360
+ dependencies: compiled.dependencies,
361
+ };
362
+ }
363
+
364
+ /** @param {string} terminal @param {readonly any[]} [args] */
365
+ #execute(terminal, args) {
366
+ const document = wrapTerminal(this.toDocument(), terminal, args);
367
+ const externalNames = [...this.#params.keys()];
368
+ const externals = Object.fromEntries(this.#params);
369
+ if (this.#sourceKind === 'provider') {
370
+ const result = this.#source.execute(document, { externals });
371
+ // A `Sequence` terminal is a VALUE — `toArray(): T[]`,
372
+ // `count(): number`. A provider whose `execute` answers a promise
373
+ // (the wasm/OPFS drivers do) cannot satisfy that, and the old seam
374
+ // let the promise through under the value's type: `count()` handed
375
+ // back a `Promise` typed `number`, and `first()` indexed the promise
376
+ // and returned `undefined` — a wrong answer with no error anywhere.
377
+ // Refuse at the seam instead, and name the surface that does work.
378
+ if (result !== null && typeof result === 'object'
379
+ && typeof (/** @type {any} */ (result).then) === 'function') {
380
+ throw new LinqRuntimeError('JL2004',
381
+ `this provider's execute() answered a promise, and a Sequence terminal is a `
382
+ + 'value — an asynchronous provider cannot back the synchronous surface. '
383
+ + 'Emit the document with toDocument() and await the provider directly, or '
384
+ + 'use a synchronous provider.');
385
+ }
386
+ return result;
387
+ }
388
+ return executeInMemory(this.#source, document, {
389
+ ...this.#options,
390
+ externalNames, externals,
391
+ });
392
+ }
393
+
394
+ /** @param {string} terminal @param {readonly any[]} [args] */
395
+ #window(terminal, args) {
396
+ // element terminals emit `[window]`, so the result is always one
397
+ // array item and element extraction is unambiguous
398
+ return /** @type {any[]} */ (this.#execute(terminal, args));
399
+ }
400
+
401
+ toArray() {
402
+ return this.#window('toArray');
403
+ }
404
+
405
+ * [Symbol.iterator]() {
406
+ yield* this.toArray();
407
+ }
408
+
409
+ first() {
410
+ const w = this.#window('first');
411
+ if (w.length === 0) throw new LinqRuntimeError('JL2001', 'first() found no element');
412
+ return w[0];
413
+ }
414
+
415
+ /** @param {any} [defaultValue] */
416
+ firstOrDefault(defaultValue) {
417
+ const w = this.#window('first');
418
+ return w.length === 0 ? defaultValue : w[0];
419
+ }
420
+
421
+ single() {
422
+ const w = this.#window('single');
423
+ if (w.length === 0) throw new LinqRuntimeError('JL2001', 'single() found no element');
424
+ if (w.length > 1) throw new LinqRuntimeError('JL2002', 'single() found more than one element');
425
+ return w[0];
426
+ }
427
+
428
+ /** @param {any} [defaultValue] */
429
+ singleOrDefault(defaultValue) {
430
+ const w = this.#window('single');
431
+ if (w.length > 1) throw new LinqRuntimeError('JL2002', 'singleOrDefault() found more than one element');
432
+ return w.length === 0 ? defaultValue : w[0];
433
+ }
434
+
435
+ last() {
436
+ const w = this.#window('last');
437
+ if (w.length === 0) throw new LinqRuntimeError('JL2001', 'last() found no element');
438
+ return w[0];
439
+ }
440
+
441
+ /** @param {any} [defaultValue] */
442
+ lastOrDefault(defaultValue) {
443
+ const w = this.#window('last');
444
+ return w.length === 0 ? defaultValue : w[0];
445
+ }
446
+
447
+ elementAt(index) {
448
+ requireIndex(index, 'elementAt');
449
+ const w = this.#window('elementAt', [index]);
450
+ if (w.length === 0) throw new LinqRuntimeError('JL2003', `elementAt(${index}) is out of range`);
451
+ return w[0];
452
+ }
453
+
454
+ /** @param {number} index @param {any} [defaultValue] */
455
+ elementAtOrDefault(index, defaultValue) {
456
+ requireIndex(index, 'elementAtOrDefault');
457
+ const w = this.#window('elementAt', [index]);
458
+ return w.length === 0 ? defaultValue : w[0];
459
+ }
460
+
461
+ count() {
462
+ return this.#execute('count');
463
+ }
464
+
465
+ sum() {
466
+ return this.#execute('sum');
467
+ }
468
+
469
+ /** C#: `Average()` over an empty sequence throws. */
470
+ average() {
471
+ const v = this.#execute('average');
472
+ if (v === undefined) throw new LinqRuntimeError('JL2001', 'average() of an empty sequence');
473
+ return v;
474
+ }
475
+
476
+ min() {
477
+ const v = this.#execute('min');
478
+ if (v === undefined) throw new LinqRuntimeError('JL2001', 'min() of an empty sequence');
479
+ return v;
480
+ }
481
+
482
+ max() {
483
+ const v = this.#execute('max');
484
+ if (v === undefined) throw new LinqRuntimeError('JL2001', 'max() of an empty sequence');
485
+ return v;
486
+ }
487
+
488
+ /** `any()` is existence; `any(pred)` is the `$some` quantifier.
489
+ * @param {(...roots: any[]) => any} [predicate] */
490
+ any(predicate) {
491
+ if (predicate === undefined) return this.#execute('exists');
492
+ return this.#execute('some', [this.#capture(predicate)]);
493
+ }
494
+
495
+ /** The `$every` quantifier (vacuously true over the empty sequence). */
496
+ all(predicate) {
497
+ return this.#execute('every', [this.#capture(predicate)]);
498
+ }
499
+
500
+ //#endregion
501
+ }
502
+
503
+ /**
504
+ * Build a sequence over an iterable or a provider (D2). Dispatch
505
+ * happens ONCE, here: an `execute` duck is a provider and is never
506
+ * enumerated locally; any iterable gets the in-memory reference
507
+ * semantics; anything else is `JL0001` now, not at enumeration time.
508
+ * @param {any} source
509
+ * @param {{ compileTypeTest?: any, functions?: any, collations?: any,
510
+ * pathFunctions?: any, limits?: any, registry?: object }} [options] -
511
+ * the engine registries this sequence compiles against, under the
512
+ * engine's own option names: `compileTypeTest` enables `ofType`/`cast`,
513
+ * `collations` makes `orderBy(..., {collation})` executable in memory,
514
+ * `functions`/`pathFunctions` make `$call` and custom path functions
515
+ * resolvable, and `limits` bounds step and result counts. Pass
516
+ * `registry` when the hooks are rebuilt per call, so compiled documents
517
+ * still share a cache partition.
518
+ * @returns {Sequence}
519
+ */
520
+ export function from(source, options = {}) {
521
+ return new Sequence(source, classifySource(source), '$[*]', [], new Map(), options);
522
+ }
523
+
524
+ /**
525
+ * Attach a hand-written (or stored) query document to a source. The
526
+ * document's result is the item sequence; further operators chain over
527
+ * it. A version envelope is unwrapped so the expression embeds.
528
+ * @param {any} source - iterable or provider, as `from`
529
+ * @param {any} document - a Jaren query document
530
+ * @param {{ compileTypeTest?: any, functions?: any, collations?: any,
531
+ * pathFunctions?: any, limits?: any, registry?: object }} [options] -
532
+ * as {@link from}. A SAVED document is the case `limits` exists for:
533
+ * bound its steps and results before running it.
534
+ * @returns {Sequence}
535
+ */
536
+ export function fromDocument(source, document, options = {}) {
537
+ let root = document;
538
+ if (root !== null && typeof root === 'object' && !Array.isArray(root)
539
+ && Object.hasOwn(root, '$expr')) {
540
+ root = root.$expr;
541
+ }
542
+ return new Sequence(source, classifySource(source), root, [], new Map(), options);
543
+ }
package/src/sources.js ADDED
@@ -0,0 +1,98 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Async source adapters (LINQ-FORMAT.md §12): everything
4
+ * `fromAsync` accepts normalizes to "a factory of async iterators" —
5
+ * a fresh iterator per enumeration, so the deferred re-enumeration
6
+ * contract carries over exactly (a one-shot generator object simply
7
+ * exhausts, the same way it does under sync `from`).
8
+ *
9
+ * Shipped shapes: any `AsyncIterable`, any sync iterable (wrapped), a
10
+ * CURSOR (`{ next(): Promise<{done, value}>, return?() }` — the shape
11
+ * the SQL provider's row iterator implements later), and a push-queue
12
+ * for feed/end-style readers that have no pull protocol of their own.
13
+ */
14
+
15
+ import { LinqBuildError } from './errors.js';
16
+
17
+ /**
18
+ * Normalize an async source into an iterator factory, or throw
19
+ * `JL0001` — at `fromAsync()` time, never at enumeration time.
20
+ * @param {any} source
21
+ * @returns {() => AsyncIterator<any>}
22
+ */
23
+ export function adaptAsyncSource(source) {
24
+ if (source != null) {
25
+ if (typeof source[Symbol.asyncIterator] === 'function') {
26
+ return () => source[Symbol.asyncIterator]();
27
+ }
28
+ if (typeof source[Symbol.iterator] === 'function' && typeof source !== 'string') {
29
+ return () => (async function* () { yield* source; })();
30
+ }
31
+ if (typeof source.next === 'function') {
32
+ // the cursor shape: already an (async) iterator
33
+ return () => /** @type {AsyncIterator<any>} */ (source);
34
+ }
35
+ }
36
+ throw new LinqBuildError('JL0001',
37
+ 'fromAsync() needs an async iterable, an iterable, a cursor ({ next, return? }) or a push queue');
38
+ }
39
+
40
+ /**
41
+ * A push→pull adapter for feed/end readers (josl's push parsers have
42
+ * deliberately no backpressure protocol, so the queue is the boundary
43
+ * where one appears): `feed(value)` enqueues and returns `false` once
44
+ * the queue holds more than `highWaterMark` items — a HINT to pause,
45
+ * never a hard stop — and `end(error?)` closes the stream. The queue
46
+ * itself is the async-iterable to hand to `fromAsync`.
47
+ * @param {{ highWaterMark?: number }} [options]
48
+ * @returns {{ feed: (value: any) => boolean, end: (error?: unknown) => void,
49
+ * [Symbol.asyncIterator]: () => AsyncIterator<any> }}
50
+ */
51
+ export function createPushQueue(options = {}) {
52
+ const highWaterMark = options.highWaterMark ?? 1024;
53
+ if (!Number.isInteger(highWaterMark) || highWaterMark < 1) {
54
+ throw new LinqBuildError('JL0005', 'highWaterMark must be a positive integer');
55
+ }
56
+ /** @type {any[]} */
57
+ const buffer = [];
58
+ /** @type {(() => void) | null} */
59
+ let wake = null;
60
+ let ended = false;
61
+ /** @type {unknown} */
62
+ let failure = null;
63
+
64
+ function signal() {
65
+ if (wake !== null) {
66
+ const w = wake;
67
+ wake = null;
68
+ w();
69
+ }
70
+ }
71
+
72
+ return {
73
+ feed(value) {
74
+ if (ended) throw new LinqBuildError('JL0005', 'feed() after end()');
75
+ buffer.push(value);
76
+ signal();
77
+ return buffer.length <= highWaterMark;
78
+ },
79
+ end(error) {
80
+ ended = true;
81
+ failure = error;
82
+ signal();
83
+ },
84
+ async* [Symbol.asyncIterator]() {
85
+ for (;;) {
86
+ if (buffer.length > 0) {
87
+ yield buffer.shift();
88
+ continue;
89
+ }
90
+ if (ended) {
91
+ if (failure !== undefined && failure !== null) throw failure;
92
+ return;
93
+ }
94
+ await new Promise((resolve) => { wake = () => resolve(undefined); });
95
+ }
96
+ },
97
+ };
98
+ }