@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.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @jarenjs/linq
2
+
3
+ A C#-familiar fluent query surface whose output is a **plain JSON
4
+ query document**. You write `from(users).where(u =>
5
+ u.age.gt(21)).orderBy(u => u.name)`; what exists afterwards is data —
6
+ inspectable, serializable, executable by the `@jarenjs/json` engine in
7
+ memory, streamed over a cursor, or pushed into a database by any
8
+ provider. The chain is the pen; the document is the deliverable.
9
+
10
+ ```js
11
+ import { from } from '@jarenjs/linq';
12
+
13
+ const adults = from(users)
14
+ .where((u) => u.age.gt(21))
15
+ .orderBy((u) => u.name)
16
+ .select((u) => ({ id: u.id, name: u.name }));
17
+
18
+ adults.toArray(); // runs in memory, deferred until now
19
+ adults.toDocument(); // { $for: { it: '$[*]' }, $where: { $gt: ['$it.age', 21] }, … }
20
+ ```
21
+
22
+ **The C# comparison, stated honestly.** The operator names, deferred
23
+ execution and the query-as-data idea are LINQ's. What differs: capture
24
+ is a recording proxy, never source-text inspection, so callbacks must
25
+ use the expression surface (`u.age.gt(21)`, not `u.age > 21` — a
26
+ JavaScript proxy cannot overload `>`); the operator vocabulary is the
27
+ query engine's, closed and documented in the
28
+ [mapping table](docs/LINQ-FORMAT.md); and `IQueryable`'s role is
29
+ played by the provider seam below.
30
+
31
+ - **Deferred and immutable.** A `Sequence` holds a stage list; nothing
32
+ runs until a terminal (`toArray`, `first`, `count`, …). Every
33
+ operator returns a new sequence.
34
+ - **Typed where it counts.** Hand-authored declarations type the
35
+ common path precisely and degrade to honest `unknown` — never a
36
+ wrong type — with runtime twins pinning every claim.
37
+ - **The async story (the obvious objection, answered).** `fromAsync`
38
+ runs the SAME operator set over cursors and streams — async is a
39
+ boundary, not a colour (the same chain emits a byte-identical
40
+ document through both drivers, test-pinned). Element-wise async
41
+ work happens in exactly one place, `mapAsync`, with a REQUIRED
42
+ concurrency bound and the `parallel`/`concat`/`switch`/`exhaust`
43
+ vocabulary; barrier operators buffer and run through the one engine
44
+ so streaming answers equal in-memory answers by construction.
45
+ - **The provider contract.** Any object with
46
+ `execute(queryDocument, { externals })` is a provider.
47
+ `@jarenjs/db` implements it — a chain over a SQLite-backed
48
+ collection pushes to SQL with no import edge in either direction.
49
+ `mapAsync` splits a provider chain into a pushed prefix and a local
50
+ residual, and `explain()` shows the split.
51
+
52
+ ## What this is not
53
+
54
+ Not an ORM — entities, storage and migrations live in `@jarenjs/db`.
55
+ Not expression trees over arbitrary methods — the vocabulary is the
56
+ query engine's, and a construct outside it fails loudly at build time
57
+ with a coded error (`JL0001`–`JL0006`) rather than guessing. Not a
58
+ general lazy-iterable library — if you don't want a query document,
59
+ you don't want this package.
60
+
61
+ The normative mapping — every operator, its emitted phrase, and the
62
+ deliberate deviations — is [docs/LINQ-FORMAT.md](docs/LINQ-FORMAT.md);
63
+ internals are in [ARCHITECTURE.md](ARCHITECTURE.md).
@@ -0,0 +1,382 @@
1
+ # The Jaren LINQ surface (normative)
2
+
3
+ Version 0.1. The key words MUST, MUST NOT, SHOULD and MAY are to be
4
+ interpreted as described in RFC 2119.
5
+
6
+ ## 1. Scope
7
+
8
+ `@jarenjs/linq` is a fluent front-end to the Jaren JSON Query language
9
+ ([QUERY-FORMAT.md](../../json/docs/QUERY-FORMAT.md)): a C#-familiar
10
+ method chain whose expressions are CAPTURED as plain query documents
11
+ and executed deferred — over any iterable in memory, or by any
12
+ **provider** exposing `execute(document, options)` (§8). The builder
13
+ emits the query language and nothing else; there is no second grammar,
14
+ no private protocol, and no `Function.prototype.toString` anywhere.
15
+
16
+ What this package is NOT: it is not an ORM (storage is `@jarenjs/db`'s
17
+ job), it does not evaluate JavaScript callbacks per element (callbacks
18
+ run ONCE, at build time, against recording proxies), and it promises
19
+ nothing the query grammar cannot express — §4 records every such gap
20
+ as `unsupported`, by name.
21
+
22
+ ## 2. The surface
23
+
24
+ ```js
25
+ import { from } from '@jarenjs/linq';
26
+
27
+ const adults = from(users)
28
+ .where((u) => u.age.gt(21))
29
+ .orderBy((u) => u.name)
30
+ .select((u) => ({ id: u.id, name: u.name }));
31
+
32
+ adults.toArray(); // executes in memory
33
+ adults.toDocument(); // the SAME query, as one JSON document:
34
+ // { "$for": { "it": "$[*]" },
35
+ // "$where": { "$gt": ["$it.age", 21] },
36
+ // "$orderby": { "$key": "$it.name" },
37
+ // "$return": { "id": "$it.id", "name": "$it.name" } }
38
+ ```
39
+
40
+ - `from(source, options?)` — `source` is any iterable (arrays,
41
+ strings, generators, Sets…) or a provider (§8); anything else is
42
+ `JL0001` at `from()` time, never at enumeration time.
43
+ `options.compileTypeTest` enables the schema operators behind
44
+ `ofType`/`cast` (§4); absent, those two are `JL0003` with the fix in
45
+ the message.
46
+ - `fromDocument(source, document, options?)` — attach a hand-written
47
+ or stored query document; its result is the item sequence and every
48
+ operator chains over it (a `{$query, $expr}` envelope is unwrapped).
49
+ - Every operator returns a NEW immutable sequence (§5); terminal
50
+ operations execute (§6).
51
+
52
+ ## 3. Expression capture
53
+
54
+ A predicate or projection callback receives a **recording proxy** per
55
+ binding (and the parameters proxy last, §7). Member access records a
56
+ path segment; a method call records an operator; the callback's return
57
+ value becomes the expression:
58
+
59
+ - `u.a.b` records the path `$it.a.b`; `u.list.at(0)` records
60
+ `$it.list[0]` (negative integers count from the end);
61
+ `u.list.all()` records `$it.list[*]`; a key that is not an
62
+ identifier — or one that collides with a method name — goes through
63
+ `u.get('odd key')`.
64
+ - Method names SHADOW member access: `u.eq` is the operator, never
65
+ the member. `u.get('eq')` reaches the member.
66
+ - Returned object literals become constructors: plain-keyed objects
67
+ are Rule 1 map constructors, arrays are Rule 3 array constructors,
68
+ and a data object with `$`-prefixed keys embeds through `$map`.
69
+ Literal strings embed with the `$$` escape when they start with
70
+ `$`; plain data trees embed as `$const`.
71
+ - A proxy belongs to exactly ONE capture. Storing one and replaying
72
+ it into a later operator is `JL0002` — the emitted document would
73
+ silently reference the wrong binding, so the build fails instead.
74
+ (`===` between proxies is untrappable and therefore undetectable;
75
+ do not compare proxies.)
76
+ - The item binding is always named `it` in the emitted document
77
+ (nested phrases shadow it legally), so captured expressions read
78
+ `$it.…` at every depth and the document stays hand-readable.
79
+
80
+ ## 4. The mapping table
81
+
82
+ Status vocabulary: **native** (emits the named construct), **emulated**
83
+ (emits a composition with identical semantics), **unsupported** (throws
84
+ a coded error naming the reason — an honest row beats a silently wrong
85
+ emission). The *typing* column is the intended TypeScript signature
86
+ shipped by the typed surface order; an operator whose signature cannot
87
+ be written is an operator whose runtime shape is wrong, so the column
88
+ is part of THIS design.
89
+
90
+ | C# / LINQ | Emission | Status | Typing (element `T`) |
91
+ |---|---|---|---|
92
+ | `Where` | FLWOR `$where` | native | `(e: Expr<T>) => Expr<boolean>` → `Seq<T>` |
93
+ | `Select` | `$return` constructor | native | `(e: Expr<T>) => Expr<R>` → `Seq<R>` |
94
+ | `SelectMany` | `$return` (a multi-item projection flattens per tuple) | native | `(e: Expr<T>) => Expr<R[]>` → `Seq<R>` |
95
+ | `OrderBy` / `OrderByDescending` | `$orderby` key spec (`$dir`; `$empty`/`$collation` via `options`) | native | `(e: Expr<T>) => Expr<K>` → `Seq<T>` |
96
+ | `ThenBy` / `ThenByDescending` | appended `$orderby` spec; must directly follow `orderBy*` (`JL0005`) | native | as `OrderBy` |
97
+ | `GroupBy` | `$groupby`; downstream items are `{ key, items }` | native | `(e: Expr<T>) => Expr<K>` → `Seq<{key: K, items: T[]}>` |
98
+ | `Join` | nested `$for` + `$where` `$eq` — the engine rewrites this shape to a HASH JOIN (compile-time, QUERY-FORMAT §6), which is why it is fast. Both sides MUST derive from the same source in 0.1 (`JL0005`): a query document reads one input; the relational order lifts this | native | `(inner: Seq<U>, ok, ik, (o: Expr<T>, i: Expr<U>) => Expr<R>)` → `Seq<R>` |
99
+ | `GroupJoin` | projection over a correlated inner phrase (the matching group as an expression: `(u, g) => ({ n: g.count() })`); same-source rule as `Join` | emulated | `(inner: Seq<U>, ok, ik, (o: Expr<T>, g: Expr<U[]>) => Expr<R>)` → `Seq<R>` |
100
+ | `Skip` / `Take` | `$subsequence` | native | `(n: number)` → `Seq<T>` |
101
+ | `Distinct` | `$distinct` (deep structural equality — the grouping relation) | native | `()` → `Seq<T>` |
102
+ | `Reverse` | `$reverse` | native | `()` → `Seq<T>` |
103
+ | `Count` / `Sum` / `Average` / `Min` / `Max` | §8.8 aggregates (`Average` → `$avg`) | native | `count(): number`; `sum(): number`; `average/min/max(): number` (throw `JL2001` on empty; `min`/`max` follow the operand family) |
104
+ | `Any()` | `$exists` | native | `(): boolean` |
105
+ | `Any(pred)` / `All(pred)` | `$some` / `$every` quantifier phrase | native | `(pred): boolean` (`all` vacuously true on empty) |
106
+ | `Aggregate(seed, fn)` | `$fold` — the accumulator clause | native | `(seed: A, (acc: Expr<A>, e: Expr<T>) => Expr<A>): A` |
107
+ | `Aggregate(fn)` (unseeded) | — JSON cannot spell "the implicit first element" as a lambda seed | unsupported (`JL0005`) | — |
108
+ | `First` / `FirstOrDefault` | `[ $subsequence [expr, 0, 1] ]` window | native | `(): T` (`JL2001` on empty) / `(d?): T \| D` |
109
+ | `Single` / `SingleOrDefault` | `[ $subsequence [expr, 0, 2] ]` window | native | `(): T` (`JL2001`/`JL2002`) / `(d?): T \| D` (`JL2002` on 2+) |
110
+ | `Last` / `LastOrDefault` | `[ $subsequence [$reverse expr, 0, 1] ]` | native | as `First` |
111
+ | `ElementAt` / `ElementAtOrDefault` | `[ $subsequence [expr, i, 1] ]` | native | `(i): T` (`JL2003` out of range) / `(i, d?)` |
112
+ | `Concat` | `$seq` (a constant array's elements join the stream) | native | `(other: Seq<T> \| T[])` → `Seq<T>` |
113
+ | `DefaultIfEmpty` | `$default` | native | `(fallback?: T)` → `Seq<T>` |
114
+ | `OfType<S>` | `$valid` filter with a JSON Schema literal | native | `(schema)` → `Seq<S>`; needs `compileTypeTest` (`JL0003`) |
115
+ | `Cast<S>` | `$assert` per item | native | `(schema)` → `Seq<S>`; needs `compileTypeTest` (`JL0003`) |
116
+ | `Zip` | — no positional co-iteration in the grammar | unsupported (`JL0006`) | — |
117
+ | expression methods | `eq ne lt le gt ge` → `$eq…$ge`; `and or not`; `add sub mul div idiv mod neg`; `startsWith endsWith contains matches upper lower length concat substring replace` → §8.7; `count sum avg min max` → §8.8 (aggregates as expressions, e.g. over a group); `year month day epoch` → §8.13; `exists isEmpty`; `at all get` | native | on `Expr<…>`, per the typed-surface order |
118
+ | spatial family (§8.14) | — surface deferred to the relational order; hand-write the document (`fromDocument`) today | unsupported (`JL0006`-adjacent: no methods exist yet) | — |
119
+
120
+ ## 5. Deferred execution and re-enumeration
121
+
122
+ Every operator returns a new immutable `Sequence`; NOTHING runs until a
123
+ terminal operation. A sequence may be enumerated repeatedly and **each
124
+ enumeration re-reads the source** — the C# contract, and the one that
125
+ surprises people:
126
+
127
+ ```js
128
+ const rows = [1, 2, 3];
129
+ const q = from(rows).where((n) => n.gt(1));
130
+ q.toArray(); // [2, 3]
131
+ rows.push(4);
132
+ q.toArray(); // [2, 3, 4] — the source was read AGAIN
133
+ ```
134
+
135
+ The compiled query is shared through a bounded cache keyed by the
136
+ document's COMPLETE structural identity (`semanticKey`), so
137
+ re-enumeration is cheap without pretending the results are frozen.
138
+ A 32-bit fingerprint would not do here: it collides after tens of
139
+ thousands of documents, and a collision means one query runs another
140
+ query's compiled program — wrong rows, cache hit reported, nothing said.
141
+ `for…of` a sequence iterates `toArray()`'s result (one enumeration per
142
+ loop).
143
+
144
+ **`toDocument()` is a deep snapshot.** A sequence is immutable, so the
145
+ document it hands out is an independent tree: writing into a returned
146
+ document cannot change what a later enumeration answers.
147
+
148
+ **A `null` a callback returns is a VALUE, not an absent clause.**
149
+ `where(() => null)` filters everything out (null is not true),
150
+ `select(() => null)` projects nulls, `groupBy(() => null)` is one
151
+ null-keyed group, and a null seed still folds. The emitted document
152
+ carries the clause with its null in place.
153
+
154
+ **A captured constant crosses a real JSON boundary.** The query data
155
+ model is JSON, so a `Date`, `Map`, `Set`, `RegExp` or class instance is
156
+ refused (`JL0005`) rather than embedded — `Object.keys` reports nothing
157
+ for them, so they would embed as `{}` and the query would compare against
158
+ an empty object. `NaN` and `±Infinity` are refused for the same reason
159
+ (JSON has neither, and lenient serialization folds them into `null`), and
160
+ so is `-0`, which shares its JSON text with `0` while dividing to the
161
+ opposite infinity. Convert first — a `Date` to its ISO string or epoch
162
+ number — or bind through `params()`.
163
+
164
+ ## 6. Terminal semantics
165
+
166
+ The real C# semantics, because getting these wrong is how a
167
+ "LINQ-like" library becomes lodash with different names:
168
+
169
+ - `first()` on empty throws `JL2001`; `firstOrDefault(d)` returns `d`
170
+ (or `undefined` when omitted).
171
+ - `single()` on empty throws `JL2001`; on two-or-more throws `JL2002`;
172
+ `singleOrDefault(d)` throws on two-or-more and returns `d` on empty.
173
+ - `last()`/`lastOrDefault(d)` mirror `first` over the reversed window.
174
+ - `elementAt(i)` out of range throws `JL2003`;
175
+ `elementAtOrDefault(i, d)` returns `d`.
176
+ - `average()`, `min()` and `max()` over an empty sequence throw
177
+ `JL2001` (C# `InvalidOperationException`); `sum()` of nothing is `0`;
178
+ `count()` of nothing is `0`.
179
+ - `any()` is existence; `all(pred)` is vacuously true over the empty
180
+ sequence.
181
+
182
+ Element terminals emit their window inside an ARRAY constructor
183
+ (`[ … ]`), so the engine's result mapping (`undefined | item | items`)
184
+ can never confuse "one array-valued item" with "several items" — the
185
+ window array is always the single result and its elements are read
186
+ positionally.
187
+
188
+ ## 7. Parameters
189
+
190
+ `.params({ tenantId })` declares AND binds externals; a callback reads
191
+ them through its last argument:
192
+
193
+ ```js
194
+ from(rows)
195
+ .params({ tenantId: 'a7' })
196
+ .where((r, p) => r.tenant.eq(p.tenantId))
197
+ .toDocument();
198
+ // { "$for": { "it": "$[*]" }, "$where": { "$eq": ["$it.tenant", "$tenantId"] }, "$return": "$it" }
199
+ ```
200
+
201
+ The emitted document carries `$tenantId` as an external parameter
202
+ (QUERY-FORMAT §9) — the seam that later becomes a bound SQL parameter.
203
+ Undeclared use is `JL0004` at BUILD time with the fix in the message
204
+ (the engine would say JQ0005 at compile time; earlier and clearer
205
+ wins). The names `it`, `it2`, `acc` and `g` are RESERVED — they are the
206
+ emitted document's own binding names — and declaring them is `JL0004`.
207
+
208
+ ## 8. The provider contract
209
+
210
+ A **provider** is any object exposing:
211
+
212
+ ```
213
+ execute(queryDocument, options) -> undefined | item | items[]
214
+ ```
215
+
216
+ - `queryDocument` arrives WHOLE — a terminal hands over the full
217
+ emitted document (including the terminal's own wrapper, §6);
218
+ nothing is enumerated locally, ever.
219
+ - `options.externals` is the `{ name: value }` record of bound
220
+ parameters (§7).
221
+ - The return value uses the ENGINE's result mapping
222
+ (`undefined` = empty, a single item as itself, several items as an
223
+ array) — the in-memory runner is the reference semantics every
224
+ provider MUST match, and it implements this same interface.
225
+ - **`execute` is SYNCHRONOUS.** A `Sequence` terminal is a value —
226
+ `toArray(): T[]`, `count(): number` — so a promise cannot be returned
227
+ under that type. A provider that answers one is refused with `JL2004`
228
+ at the seam, because the alternative is not a slow answer but a wrong
229
+ one: the promise came back typed as the value, `count()` handed a
230
+ `Promise` to arithmetic, and `first()` indexed the promise and returned
231
+ `undefined`. An asynchronous provider (a wasm/OPFS driver) is reached
232
+ by emitting `toDocument()` and awaiting the provider directly.
233
+
234
+ `@jarenjs/db` implements this contract without either package
235
+ importing the other; a test double proves the document arrives whole.
236
+
237
+ ### 8.1 Compilation registries
238
+
239
+ `from(source, options)` and `fromDocument(source, doc, options)` take the
240
+ engine's own compile options, so a document that is expressible is also
241
+ executable in memory:
242
+
243
+ | option | what it enables |
244
+ |---|---|
245
+ | `compileTypeTest` | `ofType`/`cast` (the schema operators) |
246
+ | `collations` | `orderBy(…, { collation })` — a `nl` sort is `JQ0010` without it |
247
+ | `functions` | `$call` in a hand-written or saved document |
248
+ | `pathFunctions` | custom RFC 9535 path function extensions |
249
+ | `limits` | step, sequence and result bounds — the reason a SAVED document can be run at all |
250
+ | `registry` | an explicit cache-partition key, when the hooks above are rebuilt per call |
251
+
252
+ Compiled documents are cached per registry COMBINATION, not per document
253
+ alone: the same document compiles to different code with and without a
254
+ collation registry, so sharing one partition would answer a caller who
255
+ passed no collations with the compiled-with version.
256
+
257
+ ## 9. Error codes
258
+
259
+ Build errors (`LinqBuildError`; `docPath` where a document position
260
+ exists):
261
+
262
+ | Code | Condition |
263
+ |---|---|
264
+ | `JL0001` | `from()` received neither an iterable nor a provider |
265
+ | `JL0002` | an expression proxy escaped its capture callback |
266
+ | `JL0003` | `ofType`/`cast` need an injected `compileTypeTest` |
267
+ | `JL0004` | an undeclared or reserved parameter name was used |
268
+ | `JL0005` | an operator was used invalidly at build time |
269
+ | `JL0006` | an unsupported operator was invoked |
270
+
271
+ Runtime errors (`LinqRuntimeError`):
272
+
273
+ | Code | Condition |
274
+ |---|---|
275
+ | `JL2001` | `first`/`single` found no element |
276
+ | `JL2002` | `single` found more than one element |
277
+ | `JL2003` | `elementAt` is out of range |
278
+ | `JL2004` | an asynchronous provider cannot back the synchronous surface |
279
+
280
+ Engine errors (`JQ…`) from a hand-written `fromDocument` document pass
281
+ through unwrapped — they already carry their own code and `docPath`.
282
+
283
+ ## 10. The asynchronous surface: streaming and barriers
284
+
285
+ `fromAsync(source, options?)` gives the SAME operator surface over
286
+ async sources, emitting the SAME query documents — the same chain
287
+ through `from` and `fromAsync` MUST emit byte-identical documents (the
288
+ one-operator-set proof) — with terminals returning promises. The rule:
289
+ **the pipeline is synchronous, the boundaries are async.** A compiled
290
+ query never awaits; what is asynchronous is where rows come from and
291
+ where element-wise host work happens (§11).
292
+
293
+ Per operator, whether it STREAMS (per-item evaluation, flat memory) or
294
+ is a BARRIER (materialises the stream so far and runs the maximal run
295
+ of document stages through the engine over the buffer — inherent,
296
+ because the engine itself materialises for `$orderby`/`$groupby`):
297
+
298
+ | Operator | Async behaviour |
299
+ |---|---|
300
+ | `where`, `select`, `selectMany`, `ofType`, `cast` | stream (per-item compiled evaluators — the engine, one item at a time) |
301
+ | `skip`, `take` | stream; `take` CLOSES the source when satisfied |
302
+ | `distinct` | stream, with a running key set (the grouping relation: `NaN` groups with `NaN`) |
303
+ | `defaultIfEmpty` | stream (an emptiness flag) |
304
+ | `concat` | stream for a CONSTANT array; another sequence is refused (`JL0005`) — an async source is single-pass and cannot be re-iterated for a second chain |
305
+ | | on the SYNC surface, `concat` also requires the same source: a query document reads one input, so the other sequence contributes its EXPRESSION, and a foreign sequence would have that expression evaluated against THIS source — reading the wrong rows twice instead of concatenating two inputs |
306
+ | `orderBy`/`thenBy`, `groupBy`, `join`, `aggregate`, `reverse` | BARRIER, named by `explain()` with the reason |
307
+ | `count`, `any`, `all`, `first`, `single`, `elementAt` | stream with early exit where semantics allow |
308
+ | `sum`, `average`, `min`, `max`, `last` | consume the stream; the aggregate itself runs through the ENGINE over the collected items, so its semantics (type errors included) are identical to the sync surface |
309
+
310
+ **Early termination MUST close the source**: `first()`, `any()`,
311
+ `take(n)`, and an exception mid-chain all call `.return()` on the
312
+ iterator — a generator left suspended holds a file handle or a read
313
+ transaction open. `explain()` reports `{ barriers: [{ operator,
314
+ reason }], document }` — or, when a `mapAsync` sits in the chain,
315
+ `{ split: { pushed, residual } }` instead of `document`
316
+ (`toDocument()` refuses with `JL0005`: a host callback has no document
317
+ form). No silent caps, no silent buffering: if a chain materialises,
318
+ the report says which operator forced it.
319
+
320
+ Re-enumeration follows the sync contract: each enumeration calls the
321
+ source's iterator method again. A one-shot generator object simply
322
+ exhausts — the same way it does under `from`.
323
+
324
+ ## 11. The concurrency boundary
325
+
326
+ ```js
327
+ await fromAsync(rows)
328
+ .mapAsync(async (row, signal) => fetchScore(row.id, signal),
329
+ { concurrency: 8, mode: 'parallel', ordered: true })
330
+ .where((r) => r.score.gt(0.5))
331
+ .toArray();
332
+ ```
333
+
334
+ `mapAsync` is the ONE explicit boundary for element-wise asynchronous
335
+ host work. There is no parallel universe of `selectAwait`-shaped
336
+ operators; a per-element async *predicate* is `mapAsync` then `where`.
337
+
338
+ - `concurrency` is REQUIRED and MUST be a positive integer (`JL0005`)
339
+ — the unbounded default is how libraries like this take down a
340
+ downstream service.
341
+ - `mode` reuses the `createTaskEffect` vocabulary (`@jarenjs/app` §9),
342
+ deliberately, so a reader who knows one knows the other:
343
+ `parallel` (a sliding window of N), `concat` (strictly sequential),
344
+ `switch` (a newer item supersedes and ABORTS the in-flight task),
345
+ `exhaust` (items arriving while busy are dropped). The source is
346
+ pulled eagerly under `switch`/`exhaust` — that race IS the mode.
347
+ - `ordered: true` (default) preserves source order and buffers at most
348
+ `concurrency` results — the stated cost; `ordered: false` yields on
349
+ completion.
350
+ - An `AbortSignal` is threaded to every callback and aborted on early
351
+ termination and on failure. A rejected callback FAILS CLOSED: the
352
+ first failure wins, every in-flight sibling aborts, the source
353
+ closes (the `compileDag` discipline).
354
+ - `mapAsync` is NOT translatable to a provider. A provider-backed
355
+ chain that reaches it SPLITS: everything before is pushed to the
356
+ provider whole, everything after runs locally, and `explain()`
357
+ reports `{ split: { pushed, residual } }` — the same residual
358
+ honesty the SQL pushdown owes (D8), applied to the async boundary.
359
+
360
+ ## 12. The cursor contract and the source adapters
361
+
362
+ `fromAsync` accepts, in order of preference:
363
+
364
+ - any **`AsyncIterable`** (async generators, `ReadableStream` — every
365
+ target exposes `Symbol.asyncIterator` on it, josl's
366
+ `iterateCsvStream` output);
367
+ - any sync iterable (wrapped);
368
+ - a **cursor**: `{ next(): Promise<{done, value}>, return?() }` — the
369
+ shape the SQL provider's row iterator implements later, adopted
370
+ as-is;
371
+ - a **push queue** (`createPushQueue({ highWaterMark = 1024 })`) for
372
+ feed/end-style readers with no pull protocol of their own (josl's
373
+ push parsers deliberately have no backpressure protocol; the queue
374
+ is where one appears): `feed(value)` returns `false` once the queue
375
+ exceeds the mark — a pause HINT, never a hard stop — and
376
+ `end(error?)` closes (or fails) the stream. Anything else is
377
+ `JL0001` at `fromAsync()` time.
378
+
379
+ What this surface does NOT do, by design: it does not make the query
380
+ engine async (`packages/json` is untouched and strictly synchronous),
381
+ it does not add a second operator table, and it does not add
382
+ `selectAwait`/`whereAwait` variants.
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@jarenjs/linq",
3
+ "private": false,
4
+ "version": "0.34.0",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./types/index.d.ts",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "types": "./types/index.d.ts",
12
+ "default": "./src/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "types/",
18
+ "src/",
19
+ "docs/"
20
+ ],
21
+ "description": "A C#-familiar fluent LINQ surface for the Jaren suite: expression capture into plain query documents, deferred immutable sequences, and a provider seam that runs the same document in memory or anywhere else",
22
+ "author": "joham",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/jklarenbeek/jarenjs.git",
26
+ "directory": "packages/linq"
27
+ },
28
+ "license": "MIT",
29
+ "engines": {
30
+ "node": ">=24"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "registry": "https://registry.npmjs.org/"
35
+ },
36
+ "keywords": [
37
+ "jaren",
38
+ "json",
39
+ "linq",
40
+ "query",
41
+ "fluent",
42
+ "expression-tree"
43
+ ],
44
+ "scripts": {
45
+ "build": "npm run build:types",
46
+ "build:types": "tsc -p tsconfig.json",
47
+ "prepack": "npm run build:types"
48
+ },
49
+ "dependencies": {
50
+ "@jarenjs/core": "^0.34.0",
51
+ "@jarenjs/json": "^0.34.0"
52
+ }
53
+ }