@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 +63 -0
- package/docs/LINQ-FORMAT.md +382 -0
- package/package.json +53 -0
- package/src/async.js +599 -0
- package/src/concurrency.js +213 -0
- package/src/document.js +212 -0
- package/src/errors.js +94 -0
- package/src/expression.js +320 -0
- package/src/index.js +14 -0
- package/src/provider.js +166 -0
- package/src/sequence.js +543 -0
- package/src/sources.js +98 -0
- package/types/index.d.ts +430 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Expression capture (D4): a predicate or projection callback
|
|
4
|
+
* receives a RECORDING PROXY, never a stringified function. Member
|
|
5
|
+
* access records a path segment; a method call records an operator; the
|
|
6
|
+
* output is a plain Jaren query expression (QUERY-FORMAT.md §§3–8) —
|
|
7
|
+
* hand-readable JSON, nothing else.
|
|
8
|
+
*
|
|
9
|
+
* Capture is epoch-scoped: every proxy belongs to exactly one `capture`
|
|
10
|
+
* call, and using one outside it (stored and replayed into a later
|
|
11
|
+
* chain) is `JL0002` — a proxy that escaped would otherwise emit a
|
|
12
|
+
* document that silently refers to the wrong binding. (`===` between
|
|
13
|
+
* proxies is untrappable and therefore undetectable; the format doc
|
|
14
|
+
* says so.)
|
|
15
|
+
*
|
|
16
|
+
* Method names shadow member access: `u.eq` is the operator, never the
|
|
17
|
+
* data member — reach a colliding member with `u.get('eq')`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { LinqBuildError } from './errors.js';
|
|
21
|
+
|
|
22
|
+
/** The unwrap key: proxy → its internal record. */
|
|
23
|
+
const NODE = Symbol('jaren-linq-node');
|
|
24
|
+
|
|
25
|
+
/** RFC 9535 shorthand member names travel as `.name`; everything else
|
|
26
|
+
* goes through a bracketed, single-quoted selector. */
|
|
27
|
+
const SHORTHAND_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
28
|
+
|
|
29
|
+
let activeEpoch = 0;
|
|
30
|
+
|
|
31
|
+
/** @param {any} record */
|
|
32
|
+
function assertLive(record) {
|
|
33
|
+
if (record.epoch !== activeEpoch) {
|
|
34
|
+
throw new LinqBuildError('JL0002',
|
|
35
|
+
'an expression proxy escaped its capture callback; expressions cannot be stored and replayed across operators');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Embed a literal string as a query expression: plain strings are
|
|
41
|
+
* literals by Rule 2, and a string starting `$` needs the `$$` escape
|
|
42
|
+
* so it stays data.
|
|
43
|
+
* @param {string} s
|
|
44
|
+
*/
|
|
45
|
+
function embedString(s) {
|
|
46
|
+
return s.charCodeAt(0) === 0x24 ? '$' + s : s;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* True when `value` is a plain JSON tree (no proxies) that can embed
|
|
51
|
+
* verbatim.
|
|
52
|
+
*
|
|
53
|
+
* "Plain JSON" is checked, not assumed. Walking `Object.keys` alone
|
|
54
|
+
* accepted every value whose own enumerable keys happen to be JSON —
|
|
55
|
+
* which a `Date`, a `Map`, a `RegExp`, a `Set` and every class instance
|
|
56
|
+
* satisfy vacuously, because `Object.keys` reports nothing for them. Each
|
|
57
|
+
* one then embedded as `{}` and the query filtered on an empty object.
|
|
58
|
+
* The numeric domain matters just as much: `NaN` and `±Infinity` are not
|
|
59
|
+
* JSON numbers, and `-0` is a legal number that shares JSON text with
|
|
60
|
+
* `0` while dividing to the opposite infinity. So the prototype is
|
|
61
|
+
* required to be plain and the numeric domain is required to be finite,
|
|
62
|
+
* and a captured constant outside that boundary is refused at capture —
|
|
63
|
+
* where the caller can see which value it was.
|
|
64
|
+
* @param {any} value
|
|
65
|
+
*/
|
|
66
|
+
function isPlainJson(value) {
|
|
67
|
+
if (value === null) return true;
|
|
68
|
+
const type = typeof value;
|
|
69
|
+
if (type === 'string' || type === 'boolean') return true;
|
|
70
|
+
if (type === 'number') return Number.isFinite(value) && !Object.is(value, -0);
|
|
71
|
+
if (type !== 'object') return false; // undefined, function, symbol, bigint
|
|
72
|
+
if (value[NODE] !== undefined) return false;
|
|
73
|
+
const proto = Object.getPrototypeOf(value);
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
return proto === Array.prototype && value.every(isPlainJson);
|
|
76
|
+
}
|
|
77
|
+
if (proto !== Object.prototype && proto !== null) return false;
|
|
78
|
+
for (const key of Object.keys(value)) {
|
|
79
|
+
if (!isPlainJson(value[key])) return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Turn a captured callback result — a proxy, a literal, or a plain
|
|
86
|
+
* object/array tree containing proxies — into a query expression.
|
|
87
|
+
* Objects without `$`-prefixed keys become Rule 1 constructors; a
|
|
88
|
+
* `$`-keyed data object embeds through `$map` so it stays a
|
|
89
|
+
* constructor rather than colliding with the operator vocabulary;
|
|
90
|
+
* pure data trees embed as `$const`.
|
|
91
|
+
* @param {any} value
|
|
92
|
+
* @returns {any} a query expression (plain JSON)
|
|
93
|
+
*/
|
|
94
|
+
export function toExpression(value) {
|
|
95
|
+
if (value === null) return null;
|
|
96
|
+
const t = typeof value;
|
|
97
|
+
if (t === 'string') return embedString(value);
|
|
98
|
+
if (t === 'boolean') return value;
|
|
99
|
+
if (t === 'number') {
|
|
100
|
+
if (!Number.isFinite(value)) {
|
|
101
|
+
throw new LinqBuildError('JL0005',
|
|
102
|
+
`a captured expression cannot embed ${String(value)} — the query data model is `
|
|
103
|
+
+ 'JSON, which has no NaN or Infinity, and lenient serialization would fold it '
|
|
104
|
+
+ 'into null');
|
|
105
|
+
}
|
|
106
|
+
if (Object.is(value, -0)) {
|
|
107
|
+
throw new LinqBuildError('JL0005',
|
|
108
|
+
'a captured expression cannot embed -0 — it shares its JSON text with 0 while '
|
|
109
|
+
+ 'dividing to the opposite infinity, so a document holding it cannot be '
|
|
110
|
+
+ 'keyed, stored or compared faithfully; use 0, or negate at query time');
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
if (t === 'object') {
|
|
115
|
+
const record = value[NODE];
|
|
116
|
+
if (record !== undefined) {
|
|
117
|
+
assertLive(record);
|
|
118
|
+
return record.doc;
|
|
119
|
+
}
|
|
120
|
+
if (isPlainJson(value)) {
|
|
121
|
+
// verbatim data: cheaper and clearer than a constructor tree
|
|
122
|
+
return { $const: value };
|
|
123
|
+
}
|
|
124
|
+
const proto = Object.getPrototypeOf(value);
|
|
125
|
+
if (Array.isArray(value)) {
|
|
126
|
+
if (proto !== Array.prototype) {
|
|
127
|
+
throw new LinqBuildError('JL0005',
|
|
128
|
+
'a captured expression cannot embed an Array subclass instance — its behaviour '
|
|
129
|
+
+ 'is not expressible as query data');
|
|
130
|
+
}
|
|
131
|
+
return value.map(toExpression);
|
|
132
|
+
}
|
|
133
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
134
|
+
// a Date, Map, Set, RegExp or class instance: `Object.keys` reports
|
|
135
|
+
// nothing for it, so embedding it verbatim produced `{}` and the
|
|
136
|
+
// query silently compared against an empty object
|
|
137
|
+
throw new LinqBuildError('JL0005',
|
|
138
|
+
`a captured expression cannot embed a ${value.constructor?.name ?? 'non-plain'} `
|
|
139
|
+
+ 'instance — it carries no own enumerable members, so it would embed as {}. '
|
|
140
|
+
+ 'Convert it to query data first (a Date to its ISO string or epoch number, a '
|
|
141
|
+
+ 'Map to an object), or bind it through params().');
|
|
142
|
+
}
|
|
143
|
+
const keys = Object.keys(value);
|
|
144
|
+
if (keys.some((k) => k.charCodeAt(0) === 0x24)) {
|
|
145
|
+
return { $map: keys.map((k) => [embedString(k), toExpression(value[k])]) };
|
|
146
|
+
}
|
|
147
|
+
const out = {};
|
|
148
|
+
// an own `__proto__` member is DATA here; plain assignment would set
|
|
149
|
+
// the builder's prototype and drop the member
|
|
150
|
+
for (const key of keys) {
|
|
151
|
+
Object.defineProperty(out, key, {
|
|
152
|
+
value: toExpression(value[key]),
|
|
153
|
+
writable: true,
|
|
154
|
+
enumerable: true,
|
|
155
|
+
configurable: true,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
throw new LinqBuildError('JL0005',
|
|
161
|
+
`a captured expression cannot embed a ${t === 'undefined' ? 'undefined' : t} value`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Binary operator helper. @param {string} op */
|
|
165
|
+
const binary = (op) => function (/** @type {any} */ record, /** @type {any} */ operand) {
|
|
166
|
+
return makeExpr({ [op]: [record.doc, toExpression(operand)] }, record.epoch, false);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/** Unary operator helper. @param {string} op */
|
|
170
|
+
const unary = (op) => function (/** @type {any} */ record) {
|
|
171
|
+
return makeExpr({ [op]: record.doc }, record.epoch, false);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The operator methods, name → builder(record, ...args). One table so
|
|
176
|
+
* the mapping in LINQ-FORMAT.md §4 has exactly one code counterpart.
|
|
177
|
+
* Null prototype: `constructor`/`toString` must read as member access,
|
|
178
|
+
* never as inherited "methods".
|
|
179
|
+
*/
|
|
180
|
+
const METHODS = {
|
|
181
|
+
__proto__: null,
|
|
182
|
+
// §8.4 comparisons
|
|
183
|
+
eq: binary('$eq'), ne: binary('$ne'),
|
|
184
|
+
lt: binary('$lt'), le: binary('$le'),
|
|
185
|
+
gt: binary('$gt'), ge: binary('$ge'),
|
|
186
|
+
// §8.6 logic
|
|
187
|
+
and: binary('$and'), or: binary('$or'), not: unary('$not'),
|
|
188
|
+
// §8.5 arithmetic
|
|
189
|
+
add: binary('$add'), sub: binary('$sub'), mul: binary('$mul'),
|
|
190
|
+
div: binary('$div'), idiv: binary('$idiv'), mod: binary('$mod'),
|
|
191
|
+
neg: unary('$neg'),
|
|
192
|
+
// §8.2 existence
|
|
193
|
+
exists: unary('$exists'), isEmpty: unary('$empty'),
|
|
194
|
+
// §8.7 strings
|
|
195
|
+
startsWith: binary('$starts-with'), endsWith: binary('$ends-with'),
|
|
196
|
+
contains: binary('$contains'), matches: binary('$match'),
|
|
197
|
+
upper: unary('$upper'), lower: unary('$lower'),
|
|
198
|
+
length: unary('$string-length'),
|
|
199
|
+
concat: binary('$concat'),
|
|
200
|
+
substring(record, start, len) {
|
|
201
|
+
const args = len === undefined
|
|
202
|
+
? [record.doc, toExpression(start)]
|
|
203
|
+
: [record.doc, toExpression(start), toExpression(len)];
|
|
204
|
+
return makeExpr({ $substring: args }, record.epoch, false);
|
|
205
|
+
},
|
|
206
|
+
replace(record, pattern, replacement) {
|
|
207
|
+
return makeExpr(
|
|
208
|
+
{ $replace: [record.doc, toExpression(pattern), toExpression(replacement)] },
|
|
209
|
+
record.epoch, false);
|
|
210
|
+
},
|
|
211
|
+
// §8.8 aggregates as EXPRESSIONS (a group inside a projection:
|
|
212
|
+
// `(u, g) => ({ n: g.count() })`)
|
|
213
|
+
count: unary('$count'), sum: unary('$sum'), avg: unary('$avg'),
|
|
214
|
+
min: unary('$min'), max: unary('$max'),
|
|
215
|
+
// §8.13 dates (the scalar component family; the full date surface
|
|
216
|
+
// arrives with the relational order)
|
|
217
|
+
year: unary('$year'), month: unary('$month'), day: unary('$day'),
|
|
218
|
+
epoch: unary('$epoch'),
|
|
219
|
+
// path navigation
|
|
220
|
+
at(record, index) {
|
|
221
|
+
if (record.pathable && Number.isInteger(index)) {
|
|
222
|
+
return makeExpr(`${record.doc}[${index}]`, record.epoch, true);
|
|
223
|
+
}
|
|
224
|
+
return makeExpr({ $get: [record.doc, toExpression(index)] }, record.epoch, false);
|
|
225
|
+
},
|
|
226
|
+
all(record) {
|
|
227
|
+
if (!record.pathable) {
|
|
228
|
+
throw new LinqBuildError('JL0005',
|
|
229
|
+
"all() fans out a PATH ('$it.tags[*]'); it cannot follow an operator result");
|
|
230
|
+
}
|
|
231
|
+
return makeExpr(`${record.doc}[*]`, record.epoch, true);
|
|
232
|
+
},
|
|
233
|
+
get(record, name) {
|
|
234
|
+
if (typeof name === 'string' && record.pathable) {
|
|
235
|
+
return makeExpr(`${record.doc}['${name.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}']`,
|
|
236
|
+
record.epoch, true);
|
|
237
|
+
}
|
|
238
|
+
return makeExpr({ $get: [record.doc, toExpression(name)] }, record.epoch, false);
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Build one expression proxy.
|
|
244
|
+
* @param {any} doc - the expression JSON so far
|
|
245
|
+
* @param {number} epoch - the owning capture
|
|
246
|
+
* @param {boolean} pathable - whether `doc` is a pure path string that
|
|
247
|
+
* member access may extend
|
|
248
|
+
* @returns {any}
|
|
249
|
+
*/
|
|
250
|
+
function makeExpr(doc, epoch, pathable) {
|
|
251
|
+
const record = { doc, epoch, pathable };
|
|
252
|
+
return new Proxy(record, {
|
|
253
|
+
get(target, prop) {
|
|
254
|
+
if (prop === NODE) return target;
|
|
255
|
+
if (typeof prop === 'symbol') return undefined;
|
|
256
|
+
const method = METHODS[prop];
|
|
257
|
+
if (method !== undefined) {
|
|
258
|
+
return (...args) => {
|
|
259
|
+
assertLive(target);
|
|
260
|
+
return method(target, ...args);
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
assertLive(target);
|
|
264
|
+
if (target.pathable) {
|
|
265
|
+
const step = SHORTHAND_RE.test(prop)
|
|
266
|
+
? `${target.doc}.${prop}`
|
|
267
|
+
: `${target.doc}['${prop.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}']`;
|
|
268
|
+
return makeExpr(step, target.epoch, true);
|
|
269
|
+
}
|
|
270
|
+
return makeExpr({ $get: [target.doc, prop] }, target.epoch, false);
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The parameters proxy: `p.tenantId` emits the external `$tenantId` —
|
|
277
|
+
* when the name was declared via `.params({...})`. Undeclared use is
|
|
278
|
+
* `JL0004` at BUILD time, with the fix in the message (the engine
|
|
279
|
+
* would say JQ0005 at compile time; earlier and clearer beats later).
|
|
280
|
+
* @param {Set<string>} declared
|
|
281
|
+
* @param {number} epoch
|
|
282
|
+
*/
|
|
283
|
+
function makeParams(declared, epoch) {
|
|
284
|
+
return new Proxy({}, {
|
|
285
|
+
get(_target, prop) {
|
|
286
|
+
if (typeof prop === 'symbol') return undefined;
|
|
287
|
+
if (!declared.has(prop)) {
|
|
288
|
+
throw new LinqBuildError('JL0004',
|
|
289
|
+
`parameter '${prop}' is not declared — declare it first: .params({ ${prop}: value })`);
|
|
290
|
+
}
|
|
291
|
+
return makeExpr('$' + prop, epoch, true);
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Run one capture: `fn` receives a proxy per root (plus the parameters
|
|
298
|
+
* proxy last) and its result becomes an expression via
|
|
299
|
+
* {@link toExpression}. A root is a binding NAME (`'it'` → the pathable
|
|
300
|
+
* `$it`) or a `{ doc, pathable }` record for an expression-valued root
|
|
301
|
+
* (groupJoin's inner group). Proxies die with the capture — reuse is
|
|
302
|
+
* `JL0002`.
|
|
303
|
+
* @param {(...roots: any[]) => any} fn - the user callback
|
|
304
|
+
* @param {readonly (string | { doc: any, pathable: boolean })[]} roots
|
|
305
|
+
* @param {Set<string>} declaredParams
|
|
306
|
+
* @returns {any} the captured expression (plain JSON)
|
|
307
|
+
*/
|
|
308
|
+
export function captureExpression(fn, roots, declaredParams) {
|
|
309
|
+
const epoch = ++activeEpoch;
|
|
310
|
+
const proxies = roots.map((root) => (typeof root === 'string'
|
|
311
|
+
? makeExpr('$' + root, epoch, true)
|
|
312
|
+
: makeExpr(root.doc, epoch, root.pathable)));
|
|
313
|
+
proxies.push(makeParams(declaredParams, epoch));
|
|
314
|
+
try {
|
|
315
|
+
return toExpression(fn(...proxies));
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
activeEpoch++; // every proxy of this capture is now dead
|
|
319
|
+
}
|
|
320
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file @jarenjs/linq — a C#-familiar fluent surface that captures
|
|
4
|
+
* expressions as plain Jaren query documents (QUERY-FORMAT.md),
|
|
5
|
+
* executes deferred over any iterable, and hands the SAME document
|
|
6
|
+
* whole to any provider exposing `execute(document, options)` (D2:
|
|
7
|
+
* contract-level coupling, never an import edge). The normative
|
|
8
|
+
* surface, mapping table and error codes live in docs/LINQ-FORMAT.md.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { from, fromDocument, Sequence } from './sequence.js';
|
|
12
|
+
export { fromAsync, AsyncSequence } from './async.js';
|
|
13
|
+
export { createPushQueue } from './sources.js';
|
|
14
|
+
export { LinqBuildError, LinqRuntimeError, LINQ_CODES } from './errors.js';
|
package/src/provider.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The provider seam (D2): a provider is any object exposing
|
|
4
|
+
* `execute(queryDocument, options)` — contract-level coupling, never an
|
|
5
|
+
* import edge. `@jarenjs/db` will implement this interface without
|
|
6
|
+
* either package importing the other. The in-memory runner implements
|
|
7
|
+
* the SAME interface over an iterable, so it is both the reference
|
|
8
|
+
* semantics every other provider must match and the proof the seam is
|
|
9
|
+
* real.
|
|
10
|
+
*
|
|
11
|
+
* Compiled documents are shared through a bounded LRU keyed by the
|
|
12
|
+
* document's COLLISION-FREE structural identity, one cache per REGISTRY
|
|
13
|
+
* identity — the hooks change what compiles, so two different registries
|
|
14
|
+
* must not share compiled programs.
|
|
15
|
+
* A fingerprint would not do: a 32-bit content hash collides after tens
|
|
16
|
+
* of thousands of documents, and a collision here runs one query's
|
|
17
|
+
* compiled program for another query's document — silently wrong rows.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { compileJsonQuery, JsonQueryCompileError } from '@jarenjs/json/query';
|
|
21
|
+
import { createSemanticCache, createWeakCache } from '@jarenjs/core/cache';
|
|
22
|
+
import { LinqBuildError } from './errors.js';
|
|
23
|
+
|
|
24
|
+
/** Stands in for an absent hook while walking the identity chain. */
|
|
25
|
+
const NO_HOOK = Object.freeze({});
|
|
26
|
+
|
|
27
|
+
const CACHES = createWeakCache();
|
|
28
|
+
const cacheFor = () => createSemanticCache(512);
|
|
29
|
+
|
|
30
|
+
/** The root of the hook-identity chain. */
|
|
31
|
+
const REGISTRY_IDS = createWeakCache();
|
|
32
|
+
/** The token minted for each distinct COMBINATION of hook identities. */
|
|
33
|
+
const REGISTRY_TOKEN = Symbol('linq.registryToken');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The compile options a sequence forwards to the engine, beyond the
|
|
37
|
+
* document itself. `orderBy(..., {collation})` and `$call` emit perfectly
|
|
38
|
+
* good documents, and without the matching registry the in-memory
|
|
39
|
+
* compiler could only answer `JQ0010` — so a Dutch sort was expressible
|
|
40
|
+
* and not executable. These are the registries that close that gap; the
|
|
41
|
+
* engine's own option names, deliberately, so there is one vocabulary.
|
|
42
|
+
*/
|
|
43
|
+
export const COMPILE_OPTION_KEYS = Object.freeze([
|
|
44
|
+
'compileTypeTest', 'functions', 'collations', 'pathFunctions', 'limits',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Pick the forwarded compile options out of a sequence's options bag.
|
|
49
|
+
* @param {Record<string, any>} options
|
|
50
|
+
* @returns {Record<string, any>}
|
|
51
|
+
*/
|
|
52
|
+
export function compileOptionsOf(options) {
|
|
53
|
+
/** @type {Record<string, any>} */
|
|
54
|
+
const out = {};
|
|
55
|
+
for (const key of COMPILE_OPTION_KEYS) {
|
|
56
|
+
if (options[key] !== undefined) out[key] = options[key];
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A stable token for one COMBINATION of hook identities, so compiled
|
|
63
|
+
* programs are shared exactly among callers whose hooks agree.
|
|
64
|
+
*
|
|
65
|
+
* Partitioning on one hook is not enough: a document naming a `nl`
|
|
66
|
+
* collation compiles to different code with and without that registry,
|
|
67
|
+
* and a shared partition would serve the compiled-with version to a
|
|
68
|
+
* caller who passed no collations at all — which is a wrong answer, not
|
|
69
|
+
* a missing error. The chain walks every hook slot in a fixed order
|
|
70
|
+
* through WeakMaps, so a token lives exactly as long as the registries
|
|
71
|
+
* that produced it. `limits` is plain data and rides in the cache key
|
|
72
|
+
* instead of here, so a fresh `{ steps: 1000 }` literal per call does
|
|
73
|
+
* not mint a fresh partition every time.
|
|
74
|
+
* @param {Record<string, any>} options
|
|
75
|
+
* @returns {object} the partition key
|
|
76
|
+
*/
|
|
77
|
+
function registryIdentity(options) {
|
|
78
|
+
if (options.registry !== undefined) return options.registry;
|
|
79
|
+
let node = /** @type {any} */ (REGISTRY_IDS);
|
|
80
|
+
for (const key of ['compileTypeTest', 'functions', 'collations', 'pathFunctions']) {
|
|
81
|
+
const hook = options[key];
|
|
82
|
+
const slot = hook === undefined || hook === null ? NO_HOOK : hook;
|
|
83
|
+
node = node.getOrCreate(slot, () => {
|
|
84
|
+
const next = createWeakCache();
|
|
85
|
+
/** @type {any} */ (next)[REGISTRY_TOKEN] = Object.freeze({});
|
|
86
|
+
return next;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return /** @type {any} */ (node)[REGISTRY_TOKEN];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Compile a query document, shared across equal documents.
|
|
94
|
+
* @param {any} document - the emitted (or hand-written) query document
|
|
95
|
+
* @param {{ compileTypeTest?: any, functions?: any, collations?: any,
|
|
96
|
+
* pathFunctions?: any, limits?: any, registry?: object,
|
|
97
|
+
* externals: readonly string[] }} options - `registry` is the cache
|
|
98
|
+
* partition key: one object identity per distinct set of hooks, since
|
|
99
|
+
* the hooks decide what a document compiles to
|
|
100
|
+
* @returns {any} the compiled query
|
|
101
|
+
* @throws {LinqBuildError} `JL0003` when a schema operator needs the
|
|
102
|
+
* missing `compileTypeTest` hook
|
|
103
|
+
*/
|
|
104
|
+
export function compileDocument(document, options) {
|
|
105
|
+
const cache = /** @type {any} */ (CACHES.getOrCreate(registryIdentity(options), cacheFor));
|
|
106
|
+
// the externals and the limits are part of the identity: the same
|
|
107
|
+
// document compiles differently against a different set of declared
|
|
108
|
+
// names, and differently again under a step or result bound
|
|
109
|
+
return cache.getOrCreate([document, options.externals, options.limits ?? null], () => {
|
|
110
|
+
try {
|
|
111
|
+
return compileJsonQuery(document, {
|
|
112
|
+
...compileOptionsOf(options),
|
|
113
|
+
externals: options.externals,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (err instanceof JsonQueryCompileError && err.code === 'JQ0008') {
|
|
118
|
+
throw new LinqBuildError('JL0003',
|
|
119
|
+
'ofType/cast compile schema operators, which need a type-test compiler — '
|
|
120
|
+
+ 'pass options.compileTypeTest to from()/fromDocument() '
|
|
121
|
+
+ '(e.g. createTypeTestCompiler() from @jarenjs/validate/query)',
|
|
122
|
+
err.docPath, err);
|
|
123
|
+
}
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Classify a `from()` source: an `execute` duck is a PROVIDER (the
|
|
131
|
+
* document is handed over whole; nothing is enumerated locally);
|
|
132
|
+
* any iterable is in-memory. Anything else is `JL0001` — at `from()`
|
|
133
|
+
* time, not at enumeration time.
|
|
134
|
+
* @param {any} source
|
|
135
|
+
* @returns {'provider' | 'iterable'}
|
|
136
|
+
*/
|
|
137
|
+
export function classifySource(source) {
|
|
138
|
+
if (source !== null && typeof source === 'object'
|
|
139
|
+
&& typeof (/** @type {any} */ (source).execute) === 'function') {
|
|
140
|
+
return 'provider';
|
|
141
|
+
}
|
|
142
|
+
if (source != null && (typeof source === 'string'
|
|
143
|
+
|| typeof (/** @type {any} */ (source))[Symbol.iterator] === 'function')) {
|
|
144
|
+
return 'iterable';
|
|
145
|
+
}
|
|
146
|
+
throw new LinqBuildError('JL0001',
|
|
147
|
+
'from() needs an iterable or a provider exposing execute(document, options)');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The in-memory execution of one document over an iterable source:
|
|
152
|
+
* materialise (each execution re-reads the source — the deferred
|
|
153
|
+
* re-enumeration contract), compile shared, run. The signature IS the
|
|
154
|
+
* provider contract, deliberately.
|
|
155
|
+
* @param {any} source - the iterable
|
|
156
|
+
* @param {any} document
|
|
157
|
+
* @param {{ compileTypeTest?: any, externalNames: readonly string[],
|
|
158
|
+
* externals: Record<string, any> }} options
|
|
159
|
+
* @returns {any} the engine-shaped result (`undefined | item | items[]`)
|
|
160
|
+
*/
|
|
161
|
+
export function executeInMemory(source, document, options) {
|
|
162
|
+
const compiled = compileDocument(document,
|
|
163
|
+
{ ...options, externals: options.externalNames });
|
|
164
|
+
const data = Array.isArray(source) ? source : [...source];
|
|
165
|
+
return compiled(data, options.externals);
|
|
166
|
+
}
|