@orkestrel/database 0.0.1

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,3364 @@
1
+ //#region src/core/constants.ts
2
+ /**
3
+ * The primary-key column assumed when {@link TableKeys} does not name one.
4
+ *
5
+ * @remarks
6
+ * `id` is the convention IndexedDB (`keyPath: 'id'`) and SQL (`id` / rowid) both
7
+ * lean on, so a table that omits `key` keys its rows by `id`.
8
+ */
9
+ var DEFAULT_PRIMARY = "id";
10
+ /**
11
+ * The longest `LIKE` / `GLOB` pattern the wildcard matcher accepts before rejecting it.
12
+ *
13
+ * @remarks
14
+ * A ReDoS bound (AGENTS §6.5): the SA1–SA4 migration lets a model supply `list`
15
+ * criteria over the wire, so `likeMatch` / `globMatch` run attacker-controlled
16
+ * patterns. The matcher is the LINEAR greedy two-pointer wildcard match — never a
17
+ * backtracking regex (`.*`-segments-separated-by-literals against a long input is the
18
+ * catastrophic shape JS cannot bound without atomic groups), so it is O(value ×
19
+ * pattern). Capping the pattern length bounds that pattern factor, leaving a match
20
+ * linear in the value length whatever the pattern. A longer pattern throws a
21
+ * `VALIDATION` {@link DatabaseError}; the cap is generous for any legitimate search.
22
+ */
23
+ var MAX_PATTERN_LENGTH = 1024;
24
+ //#endregion
25
+ //#region src/core/errors.ts
26
+ /**
27
+ * An error thrown by the database layer.
28
+ *
29
+ * @remarks
30
+ * Carries a {@link DatabaseErrorCode} and an optional `context` bag naming the
31
+ * offending table / key. Thrown for: operating on a closed database (`CLOSED`), a
32
+ * `resolve` miss (`NOT_FOUND`), an `add` onto an existing key (`CONFLICT`), a
33
+ * row that fails its table's contract (`VALIDATION`), a cancelled operation whose
34
+ * {@link ReadOptions.signal} aborted (`ABORTED`, carrying `signal.reason` in
35
+ * `context`), an inapplicable {@link Migration} plan (`MIGRATION`), a
36
+ * driver that violates a {@link DriverInterface} invariant, thrown by the
37
+ * `conformDriver` helper (`CONFORMANCE`), and an unexpected infrastructure
38
+ * fault surfaced by a driver seam — e.g. a filesystem failure while
39
+ * persisting (`DRIVER`) — as opposed to expected domain conditions, which
40
+ * keep their specific codes.
41
+ */
42
+ var DatabaseError = class extends Error {
43
+ code;
44
+ context;
45
+ constructor(code, message, context) {
46
+ super(message);
47
+ this.name = "DatabaseError";
48
+ this.code = code;
49
+ this.context = context;
50
+ }
51
+ };
52
+ /**
53
+ * Narrow an unknown caught value to a {@link DatabaseError}.
54
+ *
55
+ * @param value - The value to test (typically a `catch` binding)
56
+ * @returns `true` when `value` is a {@link DatabaseError}
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * try {
61
+ * await users.add(row)
62
+ * } catch (error) {
63
+ * if (isDatabaseError(error) && error.code === 'CONFLICT') await users.set(row)
64
+ * }
65
+ * ```
66
+ */
67
+ function isDatabaseError(value) {
68
+ return value instanceof DatabaseError;
69
+ }
70
+ Object.freeze([
71
+ "null",
72
+ "boolean",
73
+ "object",
74
+ "array",
75
+ "number",
76
+ "integer",
77
+ "string"
78
+ ]);
79
+ /** Determine whether a value is `null`. */
80
+ function isNull(value) {
81
+ return value === null;
82
+ }
83
+ /** Determine whether a value is `undefined`. */
84
+ function isUndefined(value) {
85
+ return value === void 0;
86
+ }
87
+ /** Determine whether a value is a string. */
88
+ function isString(value) {
89
+ return typeof value === "string";
90
+ }
91
+ /** Determine whether a value is a finite number (excludes `NaN` and `±Infinity`). */
92
+ function isFiniteNumber(value) {
93
+ return typeof value === "number" && Number.isFinite(value);
94
+ }
95
+ /** Determine whether a value is a finite integer (excludes `NaN`, `±Infinity`, and fractional numbers). */
96
+ function isInteger(value) {
97
+ return Number.isInteger(value);
98
+ }
99
+ /** Determine whether a value is a boolean. */
100
+ function isBoolean(value) {
101
+ return typeof value === "boolean";
102
+ }
103
+ /**
104
+ * Determine whether a value is a non-null object.
105
+ *
106
+ * @remarks
107
+ * `true` for arrays, class instances, plain objects, `Map`, `Set`, etc. — use
108
+ * {@link isRecord} when you need a plain-record check.
109
+ */
110
+ function isObject(value) {
111
+ return typeof value === "object" && value !== null;
112
+ }
113
+ /**
114
+ * Determine whether a value is a plain record (object literal or null-prototype),
115
+ * not an array or class instance.
116
+ *
117
+ * @remarks
118
+ * Use instead of {@link isObject} to distinguish a plain `{}` /
119
+ * `Object.create(null)` from arrays, `Date`, `Map`, etc. The prototype-chain
120
+ * test is realm-agnostic: rather than comparing against the current realm's
121
+ * `Object.prototype` (which a plain object from another `vm.Context`, iframe,
122
+ * or worker would fail), it accepts any value whose prototype is `null`, OR
123
+ * whose prototype's own prototype is `null` — the shape every plain object
124
+ * has in every realm, since `Object.prototype` itself always sits one step
125
+ * above `null`. Arrays and class instances are still rejected: an array's
126
+ * prototype chain runs through `Array.prototype` before `null`, and a class
127
+ * instance's runs through the class's own prototype. The whole body runs
128
+ * inside `attempt` (AGENTS §14) so a revoked `Proxy` or a hostile
129
+ * `getPrototypeOf` trap cannot escape as a thrown error.
130
+ */
131
+ function isRecord(value) {
132
+ const outcome = attempt(() => {
133
+ if (!isObject(value) || isArray(value)) return false;
134
+ const prototype = Object.getPrototypeOf(value);
135
+ return prototype === null || Object.getPrototypeOf(prototype) === null;
136
+ });
137
+ return outcome.success && outcome.value;
138
+ }
139
+ /** Determine whether a value is an array. */
140
+ function isArray(value) {
141
+ return Array.isArray(value);
142
+ }
143
+ /**
144
+ * Determine whether a value is a cycle-safe JSON value.
145
+ *
146
+ * @remarks
147
+ * Total guard: never throws, returns `false` for cycles, functions, `Date`
148
+ * instances, class instances, `NaN`, and `±Infinity`. Arrays and plain records
149
+ * are walked with an ancestor set so recursive input fails instead of hanging.
150
+ * The whole walk runs inside `attempt` (AGENTS §14): a hostile getter on a
151
+ * record property, or a revoked `Proxy` anywhere in the structure, is caught
152
+ * and yields `false` instead of escaping as a thrown error.
153
+ *
154
+ * @param value - The value to test
155
+ * @returns `true` when the value has a JSON representation
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * isJSONValue({ nested: [1, 'x', null] }) // true
160
+ * isJSONValue(Number.NaN) // false
161
+ * ```
162
+ */
163
+ function isJSONValue(value) {
164
+ const ancestors = /* @__PURE__ */ new WeakSet();
165
+ const check = (entry) => {
166
+ if (entry === null || isString(entry) || isBoolean(entry) || isFiniteNumber(entry)) return true;
167
+ if (Array.isArray(entry)) {
168
+ if (ancestors.has(entry)) return false;
169
+ ancestors.add(entry);
170
+ const valid = entry.every(check);
171
+ ancestors.delete(entry);
172
+ return valid;
173
+ }
174
+ if (!isRecord(entry)) return false;
175
+ if (ancestors.has(entry)) return false;
176
+ ancestors.add(entry);
177
+ const valid = Object.values(entry).every(check);
178
+ ancestors.delete(entry);
179
+ return valid;
180
+ };
181
+ const outcome = attempt(() => check(value));
182
+ return outcome.success && outcome.value;
183
+ }
184
+ /**
185
+ * Invoke a callback and capture its outcome as a {@link Result}, never letting
186
+ * a throw escape.
187
+ *
188
+ * @remarks
189
+ * The single sanctioned never-throw boundary for the guards (AGENTS §14). The
190
+ * `whereOf`, `lazyOf`, and `transformOf` combinators invoke caller-supplied
191
+ * callbacks *inside* a guard body, yet a guard must NEVER throw — it returns a
192
+ * `boolean`. This converts a throwing callback into a `Failure` so the
193
+ * surrounding guard can treat it as a non-match instead of propagating the
194
+ * exception, written once and shared rather than copy-pasted as ad-hoc
195
+ * `try`/`catch`.
196
+ *
197
+ * @param callback - The callback to invoke with no arguments
198
+ * @returns A `Success` carrying the return value, or a `Failure` carrying the
199
+ * thrown reason normalised to an `Error`
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * const outcome = attempt(() => predicate(value))
204
+ * return outcome.success && outcome.value
205
+ * ```
206
+ */
207
+ function attempt(callback) {
208
+ try {
209
+ return {
210
+ success: true,
211
+ value: callback()
212
+ };
213
+ } catch (reason) {
214
+ if (reason instanceof Error) return {
215
+ success: false,
216
+ error: reason
217
+ };
218
+ let message = "Unknown thrown value";
219
+ try {
220
+ message = String(reason);
221
+ } catch {}
222
+ return {
223
+ success: false,
224
+ error: new Error(message)
225
+ };
226
+ }
227
+ }
228
+ /**
229
+ * Resolve a (possibly nested) field value from a record by a key or key path.
230
+ *
231
+ * @remarks
232
+ * A single `string` is ONE key (never split on `.`, so dotted keys are safe); a
233
+ * string array descends left-to-right through nested objects. Intermediates may
234
+ * be any object — records, class instances, or arrays indexed by string. Returns
235
+ * `undefined` the moment a segment is missing or lands on a non-object, so the
236
+ * lookup is total — even against a hostile getter or Proxy trap that throws on
237
+ * read, contained via {@link attempt} so the throw never escapes.
238
+ *
239
+ * @param record - The source record
240
+ * @param path - A property key, or a key path descending into nested objects
241
+ * @returns The resolved value, or `undefined`
242
+ *
243
+ * @example
244
+ * ```ts
245
+ * resolveField({ user: { name: 'Ada' } }, ['user', 'name']) // 'Ada'
246
+ * resolveField({ 'a.b': 1 }, 'a.b') // 1 (one key)
247
+ * resolveField({ a: 1 }, ['a', 'b']) // undefined
248
+ * ```
249
+ */
250
+ function resolveField(record, path) {
251
+ const keys = isString(path) ? [path] : path;
252
+ let current = record;
253
+ for (const key of keys) {
254
+ if (!isObject(current)) return void 0;
255
+ const container = current;
256
+ const outcome = attempt(() => Reflect.get(container, key));
257
+ if (!outcome.success) return void 0;
258
+ current = outcome.value;
259
+ }
260
+ return current;
261
+ }
262
+ /**
263
+ * Build a deterministic pseudo-random source seeded from a single number.
264
+ *
265
+ * @remarks
266
+ * A mulberry32 generator — the same seed always yields the same sequence, so
267
+ * generated seed data is reproducible across runs. Used as the default random
268
+ * source for {@link compileGenerator}, seeded from the wall clock so casual
269
+ * callers still get varied output without passing a source themselves.
270
+ *
271
+ * @param seed - The seed for the sequence
272
+ * @returns A {@link RandomFunction} returning values in `[0, 1)`
273
+ *
274
+ * @example
275
+ * ```ts
276
+ * const random = seededRandom(42)
277
+ * random() // always the same first value for seed 42
278
+ * ```
279
+ */
280
+ function seededRandom(seed) {
281
+ let state = seed >>> 0;
282
+ return () => {
283
+ state = state + 1831565813 >>> 0;
284
+ let t = state;
285
+ t = Math.imul(t ^ t >>> 15, t | 1);
286
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
287
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
288
+ };
289
+ }
290
+ function arrayOf(elementGuard) {
291
+ return (value) => {
292
+ if (!isArray(value)) return false;
293
+ const outcome = attempt(() => value.every(elementGuard));
294
+ return outcome.success && outcome.value;
295
+ };
296
+ }
297
+ /**
298
+ * Build a guard that accepts values identical (via `Object.is`) to one of the
299
+ * provided literal primitives.
300
+ *
301
+ * @example
302
+ * ```ts
303
+ * const isRole = literalOf('admin', 'member', 'guest')
304
+ * isRole('admin') // true
305
+ * isRole('owner') // false
306
+ * ```
307
+ */
308
+ function literalOf(...literals) {
309
+ return (value) => literals.some((literal) => Object.is(literal, value));
310
+ }
311
+ /**
312
+ * Build a guard that accepts plain records matching a guard shape.
313
+ *
314
+ * @remarks
315
+ * Three calling modes depending on the `optional` argument:
316
+ * - **No `optional`** — all shape keys required; extra keys rejected.
317
+ * - **`optional: K[]`** — the listed keys are optional; all others required.
318
+ * - **`optional: true`** — every shape key is optional.
319
+ *
320
+ * Key presence is tested with `Object.hasOwn`, so a shape key satisfied only by
321
+ * an inherited prototype member (`toString`, `constructor`, …) counts as absent.
322
+ * A non-object / `null` / array input returns `false` rather than throwing. The
323
+ * extra-key check only inspects `Object.keys` (string keys), so an extra
324
+ * enumerable SYMBOL key is never rejected — intentional, for JSON fidelity, and
325
+ * matches the compiled guard.
326
+ *
327
+ * @example
328
+ * ```ts
329
+ * const isUser = recordOf({ name: isString, age: isNumber })
330
+ * isUser({ name: 'Ada', age: 36 }) // true
331
+ * isUser({ name: 'Ada' }) // false — age missing
332
+ *
333
+ * const isPartial = recordOf({ name: isString, age: isNumber }, ['age'])
334
+ * isPartial({ name: 'Ada' }) // true
335
+ * ```
336
+ */
337
+ function recordOf(shape, optional) {
338
+ const allowed = /* @__PURE__ */ new Set();
339
+ for (const key in shape) if (Object.prototype.hasOwnProperty.call(shape, key)) allowed.add(key);
340
+ const optionalSet = new Set(optional === true ? [...allowed] : isArray(optional) ? optional.map((key) => String(key)) : []);
341
+ return (value) => {
342
+ if (!isRecord(value)) return false;
343
+ const outcome = attempt(() => {
344
+ for (const key of Object.keys(value)) if (!allowed.has(key)) return false;
345
+ for (const key in shape) {
346
+ if (!Object.prototype.hasOwnProperty.call(shape, key)) continue;
347
+ const present = Object.hasOwn(value, key);
348
+ if (!optionalSet.has(key) && !present) return false;
349
+ if (present) {
350
+ const guard = shape[key];
351
+ if (!guard(value[key])) return false;
352
+ }
353
+ }
354
+ return true;
355
+ });
356
+ return outcome.success && outcome.value;
357
+ };
358
+ }
359
+ function orOf(left, right) {
360
+ return (value) => left(value) || right(value);
361
+ }
362
+ function unionOf(...guards) {
363
+ return (value) => guards.some((guard) => guard(value));
364
+ }
365
+ function intersectionOf(...guards) {
366
+ return (value) => guards.every((guard) => guard(value));
367
+ }
368
+ function whereOf(base, predicate) {
369
+ return (value) => {
370
+ if (!base(value)) return false;
371
+ const outcome = attempt(() => predicate(value));
372
+ return outcome.success && outcome.value;
373
+ };
374
+ }
375
+ /**
376
+ * Build a guard that accepts finite numbers within an inclusive `[min, max]`
377
+ * range.
378
+ *
379
+ * @remarks
380
+ * Refines {@link isFiniteNumber} with the bound comparison, so `NaN` /
381
+ * `±Infinity` are rejected before any comparison runs. An absent bound never
382
+ * constrains that side. Reused for a number's own value AND, applied to a
383
+ * `.length`, for string and array length refinements — the single source of the
384
+ * bound logic shared by the compiled guard and parser (compilers.ts).
385
+ *
386
+ * @example
387
+ * ```ts
388
+ * const inRange = boundsOf(1, 5)
389
+ * inRange(3) // true
390
+ * inRange(0) // false — below min
391
+ * inRange(6) // false — above max
392
+ *
393
+ * const atLeastTwo = boundsOf(2)
394
+ * atLeastTwo(2) // true — unbounded above
395
+ * ```
396
+ */
397
+ function boundsOf(min, max) {
398
+ return whereOf(isFiniteNumber, (value) => (min === void 0 || value >= min) && (max === void 0 || value <= max));
399
+ }
400
+ /**
401
+ * Build a guard that accepts strings satisfying optional length and pattern
402
+ * refinements — `min` / `max` length and a `pattern`.
403
+ *
404
+ * @remarks
405
+ * Composes {@link isString} with {@link boundsOf} on the string's `.length` and
406
+ * an inline `pattern.test` (the same refinement {@link matchOf} performs). When all three options are absent it returns
407
+ * the bare {@link isString} guard (the unconstrained fast path), so an
408
+ * unrefined string leaf pays no wrapping cost. The single source of the string
409
+ * refinement shared by the compiled guard and parser (compilers.ts).
410
+ *
411
+ * @example
412
+ * ```ts
413
+ * const isSlug = stringOf({ min: 1, max: 32, pattern: /^[a-z-]+$/ })
414
+ * isSlug('hello-world') // true
415
+ * isSlug('') // false — below min
416
+ * isSlug('Hello') // false — pattern miss
417
+ *
418
+ * stringOf() // identical to isString
419
+ * ```
420
+ */
421
+ function stringOf(options) {
422
+ const min = options?.min;
423
+ const max = options?.max;
424
+ const pattern = options?.pattern;
425
+ if (min === void 0 && max === void 0 && pattern === void 0) return isString;
426
+ const withinLength = boundsOf(min, max);
427
+ return whereOf(isString, (value) => withinLength(value.length) && (pattern === void 0 || pattern.test(value)));
428
+ }
429
+ /**
430
+ * Extend a guard to also allow `null`.
431
+ *
432
+ * @example
433
+ * ```ts
434
+ * const isNullableString = nullableOf(isString)
435
+ * isNullableString('hi') // true
436
+ * isNullableString(null) // true
437
+ * isNullableString(42) // false
438
+ * ```
439
+ */
440
+ function nullableOf(guard) {
441
+ return (value) => value === null || guard(value);
442
+ }
443
+ /**
444
+ * Parse an unknown value to a string.
445
+ *
446
+ * @remarks
447
+ * A string is returned unchanged; a finite number is coerced to its decimal
448
+ * string (`42` → `'42'`). `NaN`, `±Infinity`, and every other type → `undefined`.
449
+ *
450
+ * @param value - The value to parse
451
+ * @returns A string, or `undefined`
452
+ */
453
+ function parseString(value) {
454
+ if (isString(value)) return value;
455
+ if (isFiniteNumber(value)) return String(value);
456
+ }
457
+ /**
458
+ * Parse an unknown value to a finite number.
459
+ *
460
+ * @remarks
461
+ * A finite number is returned unchanged; a non-blank numeric string is parsed
462
+ * via `Number(...)`. `NaN`, `±Infinity`, blank/non-numeric strings, and every
463
+ * other type → `undefined`.
464
+ *
465
+ * @param value - The value to parse
466
+ * @returns A finite number, or `undefined`
467
+ */
468
+ function parseNumber(value) {
469
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
470
+ if (isString(value)) {
471
+ if (value.trim() === "") return void 0;
472
+ const parsed = Number(value);
473
+ return Number.isFinite(parsed) ? parsed : void 0;
474
+ }
475
+ }
476
+ /**
477
+ * Parse an unknown value to a finite integer.
478
+ *
479
+ * @remarks
480
+ * Accepts whatever {@link parseNumber} accepts, then requires the result to have
481
+ * no fractional part. `3.14` / `'3.14'` → `undefined`.
482
+ *
483
+ * @param value - The value to parse
484
+ * @returns A finite integer, or `undefined`
485
+ */
486
+ function parseInteger(value) {
487
+ const parsed = parseNumber(value);
488
+ if (parsed === void 0) return void 0;
489
+ return Number.isInteger(parsed) ? parsed : void 0;
490
+ }
491
+ /**
492
+ * Parse an unknown value to a boolean.
493
+ *
494
+ * @remarks
495
+ * A boolean is returned unchanged. The strings `'true'` / `'false'` / `'1'` /
496
+ * `'0'` and the numbers `1` / `0` coerce to the matching boolean. Everything
497
+ * else → `undefined`.
498
+ *
499
+ * @param value - The value to parse
500
+ * @returns A boolean, or `undefined`
501
+ */
502
+ function parseBoolean(value) {
503
+ if (typeof value === "boolean") return value;
504
+ if (value === "true" || value === "1" || value === 1) return true;
505
+ if (value === "false" || value === "0" || value === 0) return false;
506
+ }
507
+ /**
508
+ * Parse an unknown value to a plain record — the input reference, never cloned.
509
+ *
510
+ * @param value - The value to parse
511
+ * @returns The record, or `undefined`
512
+ */
513
+ function parseRecord(value) {
514
+ return isRecord(value) ? value : void 0;
515
+ }
516
+ /**
517
+ * Validate that a {@link ContractShape} tree is well-formed — a pure recursive
518
+ * prepass run before compilation.
519
+ *
520
+ * @remarks
521
+ * Fail-fast, per AGENTS §12: a malformed shape is a programmer error, so this
522
+ * throws a plain `Error` immediately rather than surfacing as a silently-wrong
523
+ * guard, parser, schema, or generator later. Checks, recursively:
524
+ *
525
+ * - An {@link OptionalShape} is only legal as a direct object-property value —
526
+ * `optionalShape` wrapping an array item, a union variant, another
527
+ * optional/nullable's inner shape, `additionalProperties`, or the top-level
528
+ * shape all throw. An object property IS the one legal placement: its value
529
+ * is unwrapped to `.inner` before recursing, so `.inner` itself is validated
530
+ * as a normal (non-optional-wrapping) shape.
531
+ * - A {@link UnionShape} needs at least one variant; a {@link LiteralShape}
532
+ * needs at least one value and rejects non-finite (`NaN` / `Infinity` /
533
+ * `-Infinity`) number values.
534
+ * - A bounded {@link StringShape} / {@link NumberShape} / {@link ArrayShape}
535
+ * needs `min <= max` when both are set.
536
+ * - An integer {@link NumberShape} (`integer: true`) needs a non-empty integer
537
+ * range: `Math.ceil(min ?? -Infinity) <= Math.floor(max ?? Infinity)`.
538
+ * - `null` / `json` / `raw` / `boolean` are always-valid leaves. Recursion
539
+ * continues into array items, object properties (and `additionalProperties`
540
+ * when it is a shape), union variants, and optional/nullable inner shapes.
541
+ *
542
+ * @param shape - The shape to validate
543
+ * @throws {Error} When the shape is malformed
544
+ */
545
+ function validateShape(shape) {
546
+ switch (shape.type) {
547
+ case "string":
548
+ if (shape.min !== void 0 && shape.max !== void 0 && shape.min > shape.max) throw new Error("validateShape: a string shape has min greater than max");
549
+ return;
550
+ case "number":
551
+ if (shape.min !== void 0 && shape.max !== void 0 && shape.min > shape.max) throw new Error("validateShape: a number shape has min greater than max");
552
+ if (shape.integer === true) {
553
+ if (Math.ceil(shape.min ?? Number.NEGATIVE_INFINITY) > Math.floor(shape.max ?? Number.POSITIVE_INFINITY)) throw new Error("validateShape: an integer number shape has an empty integer range");
554
+ }
555
+ return;
556
+ case "boolean":
557
+ case "null":
558
+ case "json":
559
+ case "raw": return;
560
+ case "literal":
561
+ if (shape.values.length === 0) throw new Error("validateShape: a literal shape needs at least one value");
562
+ for (const value of shape.values) if (typeof value === "number" && !Number.isFinite(value)) throw new Error("validateShape: a literal shape may not contain non-finite number values");
563
+ return;
564
+ case "array":
565
+ if (shape.min !== void 0 && shape.max !== void 0 && shape.min > shape.max) throw new Error("validateShape: an array shape has min greater than max");
566
+ validateShape(shape.items);
567
+ return;
568
+ case "object": {
569
+ for (const key of Object.keys(shape.properties)) {
570
+ const child = shape.properties[key];
571
+ if (child === void 0) continue;
572
+ validateShape(child.type === "optional" ? child.inner : child);
573
+ }
574
+ const extra = shape.additionalProperties;
575
+ if (extra !== void 0 && extra !== true && extra !== false) validateShape(extra);
576
+ return;
577
+ }
578
+ case "union":
579
+ if (shape.variants.length === 0) throw new Error("validateShape: a union shape needs at least one variant");
580
+ for (const variant of shape.variants) validateShape(variant);
581
+ return;
582
+ case "optional": throw new Error("validateShape: an optional shape may only appear as a direct object-property value");
583
+ case "nullable":
584
+ validateShape(shape.inner);
585
+ return;
586
+ }
587
+ }
588
+ /**
589
+ * Compile a {@link ContractShape} into a JSON Schema document.
590
+ *
591
+ * @remarks
592
+ * Object shapes emit `additionalProperties: false` (unless opened) and list only
593
+ * required keys in `required`; nullable shapes emit an `anyOf` with `{ type:
594
+ * 'null' }`. Emission only — it never inspects a runtime value.
595
+ *
596
+ * @param shape - The shape to compile
597
+ * @returns The emitted JSON Schema
598
+ */
599
+ function compileSchema(shape) {
600
+ switch (shape.type) {
601
+ case "string": return {
602
+ type: "string",
603
+ ...shape.min !== void 0 ? { minLength: shape.min } : {},
604
+ ...shape.max !== void 0 ? { maxLength: shape.max } : {},
605
+ ...shape.pattern !== void 0 ? { pattern: shape.pattern.source } : {},
606
+ ...shape.description !== void 0 ? { description: shape.description } : {}
607
+ };
608
+ case "number": return {
609
+ type: shape.integer === true ? "integer" : "number",
610
+ ...shape.min !== void 0 ? { minimum: shape.min } : {},
611
+ ...shape.max !== void 0 ? { maximum: shape.max } : {},
612
+ ...shape.description !== void 0 ? { description: shape.description } : {}
613
+ };
614
+ case "boolean": return {
615
+ type: "boolean",
616
+ ...shape.description !== void 0 ? { description: shape.description } : {}
617
+ };
618
+ case "null": return {
619
+ type: "null",
620
+ ...shape.description !== void 0 ? { description: shape.description } : {}
621
+ };
622
+ case "json": return { ...shape.description !== void 0 ? { description: shape.description } : {} };
623
+ case "literal": return {
624
+ enum: [...shape.values],
625
+ ...shape.description !== void 0 ? { description: shape.description } : {}
626
+ };
627
+ case "array": return {
628
+ type: "array",
629
+ items: compileSchema(shape.items),
630
+ ...shape.min !== void 0 ? { minItems: shape.min } : {},
631
+ ...shape.max !== void 0 ? { maxItems: shape.max } : {},
632
+ ...shape.description !== void 0 ? { description: shape.description } : {}
633
+ };
634
+ case "object": {
635
+ const properties = {};
636
+ const required = [];
637
+ for (const key of Object.keys(shape.properties)) {
638
+ const child = shape.properties[key];
639
+ if (child === void 0) continue;
640
+ properties[key] = compileSchema(child);
641
+ if (child.type !== "optional") required.push(key);
642
+ }
643
+ const extra = shape.additionalProperties;
644
+ const additionalProperties = extra === true ? true : extra !== void 0 && extra !== false ? compileSchema(extra) : false;
645
+ return {
646
+ type: "object",
647
+ ...Object.keys(properties).length > 0 ? { properties } : {},
648
+ ...required.length > 0 ? { required } : {},
649
+ additionalProperties,
650
+ ...shape.description !== void 0 ? { description: shape.description } : {}
651
+ };
652
+ }
653
+ case "union": return {
654
+ ...shape.mode === "oneOf" ? { oneOf: shape.variants.map((variant) => compileSchema(variant)) } : { anyOf: shape.variants.map((variant) => compileSchema(variant)) },
655
+ ...shape.description !== void 0 ? { description: shape.description } : {}
656
+ };
657
+ case "optional": return compileSchema(shape.inner);
658
+ case "nullable": return { anyOf: [compileSchema(shape.inner), { type: "null" }] };
659
+ case "raw": return shape.schema;
660
+ }
661
+ }
662
+ /**
663
+ * Compile a {@link ContractShape} into a runtime type guard.
664
+ *
665
+ * @remarks
666
+ * Reuses the combinators: `literalOf` for literals, `arrayOf` for arrays,
667
+ * `recordOf` for closed objects, `unionOf` for unions, `nullableOf` for nullable,
668
+ * and `whereOf` for constraint refinement. Like every guard it is total — it
669
+ * never throws (AGENTS §14).
670
+ *
671
+ * @param shape - The shape to compile
672
+ * @returns A guard narrowing to the shape's inferred type
673
+ */
674
+ function compileGuard(shape) {
675
+ switch (shape.type) {
676
+ case "string": return stringOf({
677
+ min: shape.min,
678
+ max: shape.max,
679
+ pattern: shape.pattern
680
+ });
681
+ case "number": {
682
+ const base = shape.integer === true ? isInteger : isFiniteNumber;
683
+ if (shape.min === void 0 && shape.max === void 0) return base;
684
+ return shape.integer === true ? intersectionOf(isInteger, boundsOf(shape.min, shape.max)) : boundsOf(shape.min, shape.max);
685
+ }
686
+ case "boolean": return isBoolean;
687
+ case "null": return isNull;
688
+ case "json": return isJSONValue;
689
+ case "literal": return literalOf(...shape.values);
690
+ case "array": {
691
+ const base = arrayOf(compileGuard(shape.items));
692
+ if (shape.min === void 0 && shape.max === void 0) return base;
693
+ const withinLength = boundsOf(shape.min, shape.max);
694
+ return whereOf(base, (value) => withinLength(value.length));
695
+ }
696
+ case "object": {
697
+ const map = Object.create(null);
698
+ const optionalKeys = [];
699
+ for (const key of Object.keys(shape.properties)) {
700
+ const child = shape.properties[key];
701
+ if (child === void 0) continue;
702
+ if (child.type === "optional") {
703
+ map[key] = compileGuard(child.inner);
704
+ optionalKeys.push(key);
705
+ } else map[key] = compileGuard(child);
706
+ }
707
+ const extra = shape.additionalProperties;
708
+ if (extra === void 0 || extra === false) return optionalKeys.length > 0 ? recordOf(map, optionalKeys) : recordOf(map);
709
+ const additional = extra === true ? void 0 : compileGuard(extra);
710
+ const required = Object.keys(map).filter((key) => !optionalKeys.includes(key));
711
+ return (value) => {
712
+ if (!isRecord(value)) return false;
713
+ for (const key of required) if (!Object.hasOwn(value, key)) return false;
714
+ const outcome = attempt(() => {
715
+ for (const key of Object.keys(value)) {
716
+ const guard = Object.hasOwn(map, key) ? map[key] : void 0;
717
+ if (guard !== void 0) {
718
+ if (!guard(value[key])) return false;
719
+ } else if (additional !== void 0 && !additional(value[key])) return false;
720
+ }
721
+ return true;
722
+ });
723
+ return outcome.success && outcome.value;
724
+ };
725
+ }
726
+ case "union": return unionOf(...shape.variants.map((variant) => compileGuard(variant)));
727
+ case "optional": return orOf(isUndefined, compileGuard(shape.inner));
728
+ case "nullable": return nullableOf(compileGuard(shape.inner));
729
+ case "raw": return (_value) => true;
730
+ }
731
+ }
732
+ /**
733
+ * Compile a {@link ContractShape} into an input parser.
734
+ *
735
+ * @remarks
736
+ * Reuses the leaf parsers (`parseString` / `parseInteger` / `parseNumber` /
737
+ * `parseBoolean` / `parseRecord`) and coerces structurally. An object fails as a
738
+ * whole on any required-field failure; a union returns a guard-valid value
739
+ * unchanged, otherwise the first variant that both parses and guards wins.
740
+ *
741
+ * After coercing a leaf, it re-applies that leaf's REFINEMENTS through the same
742
+ * combinators `compileGuard` uses — `stringOf` for a string's length/pattern and
743
+ * `boundsOf` for a number's value and an array's length — so a value that coerces
744
+ * but violates a bound parses to `undefined`. The result is full parse↔guard
745
+ * soundness (AGENTS §14): a non-`undefined` parse always satisfies the contract's
746
+ * `is`, refinements included.
747
+ *
748
+ * @param shape - The shape to compile
749
+ * @returns A parser yielding the shape's inferred type or `undefined`
750
+ */
751
+ function compileParser(shape) {
752
+ switch (shape.type) {
753
+ case "string": {
754
+ if (shape.min === void 0 && shape.max === void 0 && shape.pattern === void 0) return parseString;
755
+ const guard = stringOf({
756
+ min: shape.min,
757
+ max: shape.max,
758
+ pattern: shape.pattern
759
+ });
760
+ return (value) => {
761
+ const parsed = parseString(value);
762
+ return parsed !== void 0 && guard(parsed) ? parsed : void 0;
763
+ };
764
+ }
765
+ case "number": {
766
+ const base = shape.integer === true ? parseInteger : parseNumber;
767
+ if (shape.min === void 0 && shape.max === void 0) return base;
768
+ const within = boundsOf(shape.min, shape.max);
769
+ return (value) => {
770
+ const parsed = base(value);
771
+ return parsed !== void 0 && within(parsed) ? parsed : void 0;
772
+ };
773
+ }
774
+ case "boolean": return parseBoolean;
775
+ case "null": return (value) => value === null ? null : void 0;
776
+ case "json": return (value) => isJSONValue(value) ? value : void 0;
777
+ case "literal": {
778
+ const allowed = new Set(shape.values);
779
+ return (value) => {
780
+ if (allowed.has(value)) return value;
781
+ if (isString(value)) {
782
+ const trimmed = value.trim();
783
+ if (allowed.has(trimmed)) return trimmed;
784
+ }
785
+ };
786
+ }
787
+ case "array": {
788
+ const item = compileParser(shape.items);
789
+ const unbounded = shape.min === void 0 && shape.max === void 0;
790
+ const withinLength = boundsOf(shape.min, shape.max);
791
+ return (value) => {
792
+ if (!isArray(value)) return void 0;
793
+ const result = [];
794
+ for (const entry of value) {
795
+ const parsed = item(entry);
796
+ if (parsed === void 0) return void 0;
797
+ result.push(parsed);
798
+ }
799
+ return unbounded || withinLength(result.length) ? result : void 0;
800
+ };
801
+ }
802
+ case "object": {
803
+ const entries = [];
804
+ for (const key of Object.keys(shape.properties)) {
805
+ const child = shape.properties[key];
806
+ if (child === void 0) continue;
807
+ const optional = child.type === "optional";
808
+ entries.push({
809
+ key,
810
+ parse: compileParser(optional ? child.inner : child),
811
+ optional
812
+ });
813
+ }
814
+ const known = new Set(entries.map((entry) => entry.key));
815
+ const extra = shape.additionalProperties;
816
+ const additional = extra === void 0 || extra === false || extra === true ? void 0 : compileParser(extra);
817
+ const open = extra === true || additional !== void 0;
818
+ return (value) => {
819
+ const record = parseRecord(value);
820
+ if (record === void 0) return void 0;
821
+ const outcome = attempt(() => {
822
+ const result = Object.create(null);
823
+ for (const entry of entries) {
824
+ const raw = record[entry.key];
825
+ if (raw === void 0) {
826
+ if (entry.optional) continue;
827
+ return;
828
+ }
829
+ const parsed = entry.parse(raw);
830
+ if (parsed === void 0) return void 0;
831
+ result[entry.key] = parsed;
832
+ }
833
+ if (open) for (const key of Object.keys(record)) {
834
+ if (known.has(key)) continue;
835
+ if (additional === void 0) result[key] = record[key];
836
+ else {
837
+ const parsed = additional(record[key]);
838
+ if (parsed === void 0) return void 0;
839
+ result[key] = parsed;
840
+ }
841
+ }
842
+ return result;
843
+ });
844
+ return outcome.success ? outcome.value : void 0;
845
+ };
846
+ }
847
+ case "union": {
848
+ const variants = shape.variants.map((variant) => ({
849
+ parse: compileParser(variant),
850
+ guard: compileGuard(variant)
851
+ }));
852
+ return (value) => {
853
+ for (const variant of variants) if (variant.guard(value)) return value;
854
+ for (const variant of variants) {
855
+ const parsed = variant.parse(value);
856
+ if (parsed !== void 0 && variant.guard(parsed)) return parsed;
857
+ }
858
+ };
859
+ }
860
+ case "optional": {
861
+ const inner = compileParser(shape.inner);
862
+ return (value) => value === void 0 ? void 0 : inner(value);
863
+ }
864
+ case "nullable": {
865
+ const inner = compileParser(shape.inner);
866
+ return (value) => value === null ? null : inner(value);
867
+ }
868
+ case "raw": return (value) => value;
869
+ }
870
+ }
871
+ /**
872
+ * Compile a {@link ContractShape} into a deterministic seed value.
873
+ *
874
+ * @remarks
875
+ * The same shape and the same `random` source always produce the same value, so
876
+ * seed data is reproducible. Defaults to a {@link seededRandom} source seeded
877
+ * from the wall clock when none is supplied. Throws on a degenerate empty
878
+ * `literalShape` / `unionShape`, on a pattern-constrained `stringShape` whose
879
+ * generated sample cannot satisfy the pattern, or on a `rawShape` (its embedded
880
+ * schema is arbitrary and cannot be auto-generated) — a programmer error that
881
+ * cannot generate a value (AGENTS §12). `createContract` runs
882
+ * {@link validateShape} first, so a degenerate `literalShape` / `unionShape` /
883
+ * bounded shape is normally caught there; these throws remain here as defense
884
+ * for standalone `compileGenerator` use.
885
+ *
886
+ * @param shape - The shape to generate from
887
+ * @param random - A seeded random source (defaults to `seededRandom(Date.now())`)
888
+ * @returns A value matching the shape
889
+ */
890
+ function compileGenerator(shape, random = seededRandom(Date.now())) {
891
+ switch (shape.type) {
892
+ case "string": {
893
+ const min = shape.min ?? 0;
894
+ const max = shape.max ?? Math.max(min, 12);
895
+ const length = Math.max(min, Math.min(max, 8));
896
+ const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
897
+ let value = "";
898
+ for (let index = 0; index < length; index += 1) value += alphabet[Math.floor(random() * 36)];
899
+ if (shape.pattern !== void 0 && !shape.pattern.test(value)) throw new Error("compileGenerator: a pattern-constrained string shape cannot be auto-generated — supply or verify values another way");
900
+ return value;
901
+ }
902
+ case "number": {
903
+ const min = shape.min ?? 0;
904
+ const max = shape.max ?? 100;
905
+ if (shape.integer === true) {
906
+ const lo = Math.ceil(min);
907
+ const hi = Math.floor(max);
908
+ return Math.floor(random() * (hi - lo + 1)) + lo;
909
+ }
910
+ return random() * (max - min) + min;
911
+ }
912
+ case "boolean": return random() >= .5;
913
+ case "null": return null;
914
+ case "json": {
915
+ const pick = Math.floor(random() * 5);
916
+ if (pick === 0) return null;
917
+ if (pick === 1) return random() >= .5;
918
+ if (pick === 2) return Math.floor(random() * 1e3);
919
+ if (pick === 3) {
920
+ const alphabet = "abcdefghijklmnopqrstuvwxyz";
921
+ let value = "";
922
+ for (let index = 0; index < 6; index += 1) value += alphabet[Math.floor(random() * 26)];
923
+ return value;
924
+ }
925
+ return { value: Math.floor(random() * 1e3) };
926
+ }
927
+ case "literal":
928
+ if (shape.values.length === 0) throw new Error("compileGenerator: a literal shape needs at least one value");
929
+ return shape.values[Math.floor(random() * shape.values.length)];
930
+ case "array": {
931
+ const lo = shape.min ?? Math.min(1, shape.max ?? 1);
932
+ const hi = shape.max ?? Math.max(lo, 3);
933
+ const length = Math.floor(random() * (hi - lo + 1)) + lo;
934
+ const result = [];
935
+ for (let index = 0; index < length; index += 1) result.push(compileGenerator(shape.items, random));
936
+ return result;
937
+ }
938
+ case "object": {
939
+ const result = {};
940
+ for (const key of Object.keys(shape.properties)) {
941
+ const child = shape.properties[key];
942
+ if (child === void 0) continue;
943
+ if (child.type === "optional" && random() < .3) continue;
944
+ result[key] = compileGenerator(child, random);
945
+ }
946
+ const extra = shape.additionalProperties;
947
+ if (extra !== void 0 && extra !== true && extra !== false) {
948
+ const count = 1 + Math.floor(random() * 2);
949
+ for (let index = 0; index < count; index += 1) {
950
+ const key = `key${index}`;
951
+ if (Object.hasOwn(result, key)) continue;
952
+ result[key] = compileGenerator(extra, random);
953
+ }
954
+ }
955
+ return result;
956
+ }
957
+ case "union":
958
+ if (shape.variants.length === 0) throw new Error("compileGenerator: a union shape needs at least one variant");
959
+ return compileGenerator(shape.variants[Math.floor(random() * shape.variants.length)], random);
960
+ case "optional": return compileGenerator(shape.inner, random);
961
+ case "nullable": return random() < .2 ? null : compileGenerator(shape.inner, random);
962
+ case "raw": throw new Error("compileGenerator: a raw shape embeds an arbitrary JSON Schema and cannot be auto-generated — supply values another way");
963
+ }
964
+ }
965
+ function createContract(shape) {
966
+ validateShape(shape);
967
+ const schema = compileSchema(shape);
968
+ const guard = compileGuard(shape);
969
+ const parser = compileParser(shape);
970
+ return {
971
+ schema,
972
+ is: guard,
973
+ parse(value) {
974
+ return parser(value);
975
+ },
976
+ generate(random) {
977
+ return compileGenerator(shape, random);
978
+ }
979
+ };
980
+ }
981
+ /**
982
+ * Build an {@link ObjectShape} from a property map.
983
+ *
984
+ * @remarks
985
+ * Wrap any property in {@link optionalShape} to allow its absence. By default
986
+ * the compiled guard rejects unknown keys; pass `additionalProperties` to open
987
+ * the object.
988
+ *
989
+ * @param properties - Map of property names to child shapes
990
+ * @param options - Optional `additionalProperties` and `description`
991
+ * @returns An object shape
992
+ *
993
+ * @example
994
+ * ```ts
995
+ * const user = objectShape({
996
+ * name: stringShape({ min: 1 }),
997
+ * age: integerShape({ min: 0, max: 120 }),
998
+ * bio: optionalShape(stringShape()),
999
+ * })
1000
+ * ```
1001
+ */
1002
+ function objectShape(properties, options) {
1003
+ return {
1004
+ type: "object",
1005
+ properties,
1006
+ additionalProperties: options?.additionalProperties,
1007
+ description: options?.description
1008
+ };
1009
+ }
1010
+ //#endregion
1011
+ //#region src/core/helpers.ts
1012
+ /**
1013
+ * A total ordering over arbitrary values — the comparator behind sorting and the
1014
+ * range operators.
1015
+ *
1016
+ * @remarks
1017
+ * Values of different types order by a fixed type rank (`undefined` < `null` <
1018
+ * boolean < number < string < other); same-typed values compare naturally.
1019
+ * `NaN` sorts after every other number and equal to itself, so the comparator
1020
+ * is total and never returns `NaN`.
1021
+ *
1022
+ * @param left - The left value
1023
+ * @param right - The right value
1024
+ * @returns `-1`, `0`, or `1`
1025
+ */
1026
+ function compareValues(left, right) {
1027
+ const rankOf = (value) => {
1028
+ if (value === void 0) return 0;
1029
+ if (value === null) return 1;
1030
+ if (typeof value === "boolean") return 2;
1031
+ if (typeof value === "number") return 3;
1032
+ if (typeof value === "string") return 4;
1033
+ return 5;
1034
+ };
1035
+ const leftRank = rankOf(left);
1036
+ const rightRank = rankOf(right);
1037
+ if (leftRank !== rightRank) return leftRank < rightRank ? -1 : 1;
1038
+ if (typeof left === "number" && typeof right === "number") {
1039
+ if (Number.isNaN(left) || Number.isNaN(right)) return Number.isNaN(left) ? Number.isNaN(right) ? 0 : 1 : -1;
1040
+ return left < right ? -1 : left > right ? 1 : 0;
1041
+ }
1042
+ if (typeof left === "string" && typeof right === "string") return left < right ? -1 : left > right ? 1 : 0;
1043
+ if (typeof left === "boolean" && typeof right === "boolean") return left === right ? 0 : left ? 1 : -1;
1044
+ return 0;
1045
+ }
1046
+ /**
1047
+ * Structural equality by SameValueZero leaves — the comparator behind conformance
1048
+ * checks and any test/fixture that needs "same data", not "same reference".
1049
+ *
1050
+ * @remarks
1051
+ * Primitives compare by SameValueZero (`NaN` equals itself; `+0` equals `-0`).
1052
+ * Arrays compare by index (same length, every element `deepEqual`). Plain
1053
+ * records (via `isRecord`) compare by their OWN enumerable keys: same key
1054
+ * COUNT and, for every key in `left`, `right` has that key (`Object.hasOwn`)
1055
+ * with a `deepEqual` value — so a key present with value `undefined` is NOT
1056
+ * equal to that key being absent (both differ in `Object.keys` membership).
1057
+ * Anything else (functions, class instances, mismatched shapes) falls through
1058
+ * to `false`. There is no cycle detection — a cyclic input recurses forever;
1059
+ * callers pass acyclic data (rows, plans, config).
1060
+ *
1061
+ * @param left - The left value
1062
+ * @param right - The right value
1063
+ * @returns Whether `left` and `right` are structurally equal
1064
+ *
1065
+ * @example
1066
+ * ```ts
1067
+ * deepEqual(Number.NaN, Number.NaN) // true
1068
+ * deepEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }) // true
1069
+ * deepEqual({ a: undefined }, {}) // false — present-undefined ≠ absent
1070
+ * ```
1071
+ */
1072
+ function deepEqual(left, right) {
1073
+ if (typeof left === "number" && typeof right === "number") return Number.isNaN(left) && Number.isNaN(right) || left === right;
1074
+ if (left === right) return true;
1075
+ if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((item, index) => deepEqual(item, right[index]));
1076
+ if (isRecord(left) && isRecord(right)) {
1077
+ const leftKeys = Object.keys(left);
1078
+ const rightKeys = Object.keys(right);
1079
+ if (leftKeys.length !== rightKeys.length) return false;
1080
+ return leftKeys.every((key) => Object.hasOwn(right, key) && deepEqual(left[key], right[key]));
1081
+ }
1082
+ return false;
1083
+ }
1084
+ /**
1085
+ * Match a value against a wildcard pattern in LINEAR time — the shared, ReDoS-SAFE
1086
+ * engine behind {@link likeMatch} and {@link globMatch}.
1087
+ *
1088
+ * @remarks
1089
+ * A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is CATASTROPHIC on a hostile pattern:
1090
+ * `.*` segments separated by literals, matched against a long non-matching input, blow
1091
+ * up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it
1092
+ * (AGENTS §6.5, now that the authed server runs model-supplied `list` criteria over the
1093
+ * wire). So this builds NO regex. It runs the classic GREEDY TWO-POINTER wildcard match:
1094
+ * the `any` wildcard records its position and, on a later mismatch, backtracks ONLY to
1095
+ * that last `any` (letting it absorb one more char) — so the work is O(value × pattern),
1096
+ * never the exponential / polynomial backtracking a regex would do. The pattern length
1097
+ * is capped at {@link MAX_PATTERN_LENGTH} (a `VALIDATION` {@link DatabaseError} over it),
1098
+ * bounding the pattern factor so a match stays linear in the value length whatever the
1099
+ * pattern.
1100
+ *
1101
+ * The `any` wildcard matches any run (including empty); `single` matches exactly one
1102
+ * char; every other pattern char matches itself LITERALLY (a pattern `.` / `(` / `\` is
1103
+ * a literal — the regex-metacharacter hazard is gone with the regex). `any` is tested
1104
+ * BEFORE a literal match, so a value that literally contains the wildcard char never
1105
+ * shadows the wildcard. Case folding is applied to BOTH sides when `fold` is set.
1106
+ *
1107
+ * @param value - The value to test
1108
+ * @param pattern - The wildcard pattern
1109
+ * @param any - The any-run wildcard char (`%` for `LIKE`, `*` for `GLOB`)
1110
+ * @param single - The single-char wildcard char (`_` for `LIKE`, `?` for `GLOB`)
1111
+ * @param fold - Whether to match case-INSENSITIVELY (`LIKE` folds; `GLOB` does not)
1112
+ * @returns Whether `value` matches `pattern`
1113
+ * @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
1114
+ */
1115
+ function wildcardMatch(value, pattern, any, single, fold) {
1116
+ if (pattern.length > 1024) throw new DatabaseError("VALIDATION", `Pattern exceeds the maximum length of ${MAX_PATTERN_LENGTH}`, {
1117
+ length: pattern.length,
1118
+ limit: MAX_PATTERN_LENGTH
1119
+ });
1120
+ const haystack = fold ? value.toLowerCase() : value;
1121
+ const needle = fold ? pattern.toLowerCase() : pattern;
1122
+ let vi = 0;
1123
+ let pi = 0;
1124
+ let star = -1;
1125
+ let mark = 0;
1126
+ while (vi < haystack.length) {
1127
+ const pc = pi < needle.length ? needle[pi] : void 0;
1128
+ if (pc === any) {
1129
+ star = pi;
1130
+ mark = vi;
1131
+ pi += 1;
1132
+ } else if (pc !== void 0 && (pc === single || pc === haystack[vi])) {
1133
+ vi += 1;
1134
+ pi += 1;
1135
+ } else if (star !== -1) {
1136
+ pi = star + 1;
1137
+ mark += 1;
1138
+ vi = mark;
1139
+ } else return false;
1140
+ }
1141
+ while (pi < needle.length && needle[pi] === any) pi += 1;
1142
+ return pi === needle.length;
1143
+ }
1144
+ function likeMatch(value, pattern) {
1145
+ return wildcardMatch(value, pattern, "%", "_", true);
1146
+ }
1147
+ function globMatch(value, pattern) {
1148
+ return wildcardMatch(value, pattern, "*", "?", false);
1149
+ }
1150
+ /**
1151
+ * Evaluate one {@link Condition} against a row — the per-operator predicate.
1152
+ *
1153
+ * @remarks
1154
+ * Reads the condition's column — a `FieldPath`, resolved with `resolveField` (a
1155
+ * string is one column; an array descends a nested value) — and applies the
1156
+ * operator. Range operators use {@link compareValues}; `like` / `glob` / `starts`
1157
+ * / `ends` match only strings; `any` / `none` test membership by value equality.
1158
+ * Total — a type mismatch is simply a non-match.
1159
+ *
1160
+ * @param row - The row to test
1161
+ * @param condition - The condition to apply
1162
+ * @returns Whether the row satisfies the condition
1163
+ */
1164
+ function matchesCondition(row, condition) {
1165
+ const value = resolveField(row, condition.column);
1166
+ const first = condition.values[0];
1167
+ const second = condition.values[1];
1168
+ switch (condition.operator) {
1169
+ case "equals": return compareValues(value, first) === 0;
1170
+ case "not": return compareValues(value, first) !== 0;
1171
+ case "above": return compareValues(value, first) > 0;
1172
+ case "below": return compareValues(value, first) < 0;
1173
+ case "from": return compareValues(value, first) >= 0;
1174
+ case "to": return compareValues(value, first) <= 0;
1175
+ case "between": return compareValues(value, first) >= 0 && compareValues(value, second) <= 0;
1176
+ case "like": return isString(value) && isString(first) && likeMatch(value, first);
1177
+ case "glob": return isString(value) && isString(first) && globMatch(value, first);
1178
+ case "starts": return isString(value) && isString(first) && value.startsWith(first);
1179
+ case "ends": return isString(value) && isString(first) && value.endsWith(first);
1180
+ case "any": return condition.values.some((candidate) => compareValues(value, candidate) === 0);
1181
+ case "none": return !condition.values.some((candidate) => compareValues(value, candidate) === 0);
1182
+ case "absent": return value === void 0 || value === null;
1183
+ case "present": return value !== void 0 && value !== null;
1184
+ }
1185
+ }
1186
+ /**
1187
+ * Fold a row through a list of conditions, joining each by its connector.
1188
+ *
1189
+ * @remarks
1190
+ * Evaluated left-to-right: the first condition seeds the result, and each later
1191
+ * condition combines with `&&` (`and`) or `||` (`or`). An empty list matches
1192
+ * every row. There is no operator precedence — conditions combine in the order
1193
+ * the query builder recorded them.
1194
+ *
1195
+ * @param row - The row to test
1196
+ * @param conditions - The conditions to fold
1197
+ * @returns Whether the row satisfies the combined conditions
1198
+ */
1199
+ function matchesCriteria(row, conditions) {
1200
+ let result = true;
1201
+ let seeded = false;
1202
+ for (const condition of conditions) {
1203
+ const match = matchesCondition(row, condition);
1204
+ if (!seeded) {
1205
+ result = match;
1206
+ seeded = true;
1207
+ } else result = condition.connector === "or" ? result || match : result && match;
1208
+ }
1209
+ return result;
1210
+ }
1211
+ /**
1212
+ * Filter rows by a list of conditions — the shared basis for a table's count
1213
+ * and aggregate paths (no sort/page, unlike {@link applyCriteria}).
1214
+ *
1215
+ * @remarks
1216
+ * An empty condition list matches every row (returned as-is, no copy). Folds
1217
+ * each row through {@link matchesCriteria}.
1218
+ *
1219
+ * @param rows - The rows to filter
1220
+ * @param conditions - The conditions to apply (empty matches everything)
1221
+ * @returns The matching rows
1222
+ *
1223
+ * @example
1224
+ * ```ts
1225
+ * filterRows(
1226
+ * [{ age: 30 }, { age: 12 }],
1227
+ * [{ column: 'age', operator: 'above', values: [18], connector: 'and' }],
1228
+ * ) // => [{ age: 30 }]
1229
+ * ```
1230
+ */
1231
+ function filterRows(rows, conditions) {
1232
+ if (conditions.length === 0) return rows;
1233
+ return rows.filter((row) => matchesCriteria(row, conditions));
1234
+ }
1235
+ /**
1236
+ * Sort rows by an ordering specification, leaving the input untouched.
1237
+ *
1238
+ * @remarks
1239
+ * Applies the terms in priority order — the first term that distinguishes two
1240
+ * rows decides — using {@link compareValues}, reversing for `descending`.
1241
+ *
1242
+ * @param rows - The rows to sort
1243
+ * @param order - The ordering terms in priority order
1244
+ * @returns A new, sorted array
1245
+ */
1246
+ function sortRows(rows, order) {
1247
+ const sorted = [...rows];
1248
+ sorted.sort((left, right) => {
1249
+ for (const term of order) {
1250
+ const comparison = compareValues(resolveField(left, term.column), resolveField(right, term.column));
1251
+ if (comparison !== 0) return term.direction === "descending" ? -comparison : comparison;
1252
+ }
1253
+ return 0;
1254
+ });
1255
+ return sorted;
1256
+ }
1257
+ /**
1258
+ * Apply a {@link Criteria} to rows — filter, then sort, then page.
1259
+ *
1260
+ * @remarks
1261
+ * The whole portable read pipeline in one place: conditions filter, `order`
1262
+ * sorts, and `offset` / `limit` window the result. Each step is skipped when its
1263
+ * part of the criteria is absent. The reference {@link DriverInterface} backends
1264
+ * lean on this rather than each re-deriving it.
1265
+ *
1266
+ * @param rows - The rows to process (typically a table's full `scan`)
1267
+ * @param criteria - The read specification, or `undefined` for all rows as-is
1268
+ * @returns The filtered, sorted, paged rows
1269
+ */
1270
+ function applyCriteria(rows, criteria) {
1271
+ let result = rows;
1272
+ const conditions = criteria?.conditions;
1273
+ if (conditions !== void 0 && conditions.length > 0) result = result.filter((row) => matchesCriteria(row, conditions));
1274
+ const order = criteria?.order;
1275
+ if (order !== void 0 && order.length > 0) result = sortRows(result, order);
1276
+ const offset = criteria?.offset ?? 0;
1277
+ const limit = criteria?.limit;
1278
+ if (offset > 0 || limit !== void 0) result = result.slice(offset, limit !== void 0 ? offset + limit : void 0);
1279
+ return result;
1280
+ }
1281
+ /**
1282
+ * Compute an aggregate over a column across rows.
1283
+ *
1284
+ * @remarks
1285
+ * `count` returns the row count. The numeric aggregates coerce each cell with
1286
+ * the contracts `parseNumber` (so `'42'` counts) and ignore non-numeric cells;
1287
+ * over zero numeric values they return `undefined` — the SQL `NULL` of an empty
1288
+ * aggregate.
1289
+ *
1290
+ * @param rows - The rows to aggregate (non-record entries are ignored)
1291
+ * @param operation - The aggregate to compute
1292
+ * @param column - The column to aggregate
1293
+ * @returns The aggregate value, or `undefined` when undefined for the inputs
1294
+ */
1295
+ function computeAggregate(rows, operation, column) {
1296
+ if (operation === "count") return rows.length;
1297
+ const numbers = [];
1298
+ for (const row of rows) {
1299
+ if (!isRecord(row)) continue;
1300
+ const value = parseNumber(resolveField(row, column));
1301
+ if (value !== void 0) numbers.push(value);
1302
+ }
1303
+ if (numbers.length === 0) return void 0;
1304
+ if (operation === "sum" || operation === "average") {
1305
+ const total = numbers.reduce((sum, value) => sum + value, 0);
1306
+ return operation === "average" ? total / numbers.length : total;
1307
+ }
1308
+ return operation === "minimum" ? Math.min(...numbers) : Math.max(...numbers);
1309
+ }
1310
+ /**
1311
+ * Read a row's primary key from a column, when it is a usable {@link Key}.
1312
+ *
1313
+ * @param row - The row to read
1314
+ * @param column - The primary-key column name
1315
+ * @returns The key (a string or finite number), or `undefined`
1316
+ */
1317
+ function extractKey(row, column) {
1318
+ const value = row[column];
1319
+ if (isString(value)) return value;
1320
+ if (isFiniteNumber(value)) return value;
1321
+ }
1322
+ /**
1323
+ * Map a column's {@link ContractShape} to its portable {@link ColumnType} — the
1324
+ * value a `TableSchema` carries so a native backend can declare a real column.
1325
+ *
1326
+ * @remarks
1327
+ * `string` → `text`; `number` → `integer` when the shape is integer-only, else
1328
+ * `real`; `boolean` → `boolean`. A `literal` takes the type of its values
1329
+ * (all-boolean → `boolean`, all-integer → `integer`, mixed/fractional numbers →
1330
+ * `real`, anything else → `text`). `optional` / `nullable` unwrap to their inner
1331
+ * type (nullability is tracked separately). `null` / `object` / `array` / `union` /
1332
+ * `json` / `raw` → `json`: a backend stores them as JSON text and can `json_extract`
1333
+ * for nested `FieldPath` queries. A scan-only backend ignores the result.
1334
+ *
1335
+ * @param shape - The column's contract shape
1336
+ * @returns The portable column type
1337
+ *
1338
+ * @example
1339
+ * ```ts
1340
+ * shapeToColumnType(stringShape()) // 'text'
1341
+ * shapeToColumnType(integerShape()) // 'integer'
1342
+ * shapeToColumnType(optionalShape(integerShape())) // 'integer'
1343
+ * shapeToColumnType(objectShape({ a: stringShape() })) // 'json'
1344
+ * ```
1345
+ */
1346
+ function shapeToColumnType(shape) {
1347
+ switch (shape.type) {
1348
+ case "string": return "text";
1349
+ case "number": return shape.integer === true ? "integer" : "real";
1350
+ case "boolean": return "boolean";
1351
+ case "literal":
1352
+ if (shape.values.every((value) => typeof value === "boolean")) return "boolean";
1353
+ if (shape.values.every((value) => typeof value === "number")) return shape.values.every((value) => Number.isInteger(value)) ? "integer" : "real";
1354
+ return "text";
1355
+ case "optional":
1356
+ case "nullable": return shapeToColumnType(shape.inner);
1357
+ case "null":
1358
+ case "object":
1359
+ case "array":
1360
+ case "union":
1361
+ case "json":
1362
+ case "raw": return "json";
1363
+ }
1364
+ }
1365
+ /**
1366
+ * Throw when an {@link ReadOptions.signal | AbortSignal} has fired — the shared
1367
+ * cancellation gate checked at operation boundaries and between streamed rows.
1368
+ *
1369
+ * @remarks
1370
+ * A no-op for `undefined` or a live signal, so callers thread `options?.signal`
1371
+ * straight through. When the signal has aborted, throws an `ABORTED`
1372
+ * {@link DatabaseError} carrying the signal's `reason` in its context — callers
1373
+ * mint signals with whatever tool they like (`AbortSignal.timeout(ms)`,
1374
+ * `new AbortController()`, `@orkestrel/abort`).
1375
+ *
1376
+ * @param signal - The signal to check, if any
1377
+ * @returns Nothing — returns normally while the signal is live
1378
+ * @throws An `ABORTED` {@link DatabaseError} when the signal has aborted
1379
+ *
1380
+ * @example
1381
+ * ```ts
1382
+ * import { checkAbort } from '@orkestrel/database'
1383
+ *
1384
+ * const controller = new AbortController()
1385
+ * checkAbort(controller.signal) // returns
1386
+ * controller.abort('too slow')
1387
+ * checkAbort(controller.signal) // throws DatabaseError('ABORTED', …)
1388
+ * ```
1389
+ */
1390
+ function checkAbort(signal) {
1391
+ if (signal?.aborted) throw new DatabaseError("ABORTED", "Operation aborted", { reason: signal.reason });
1392
+ }
1393
+ /**
1394
+ * Structurally diff a deployed and a declared table set into a {@link Migration}
1395
+ * plan.
1396
+ *
1397
+ * @remarks
1398
+ * Tables present in `declared` but not `deployed` become `table.add` steps
1399
+ * (carrying the full declared {@link TableSchema}); tables present in
1400
+ * `deployed` but not `declared` become `table.remove` steps. Tables present in
1401
+ * both are diffed column-by-column (by name) and index-group-by-index-group
1402
+ * (by deep equality of the column-name array), each producing `column.add` /
1403
+ * `column.remove` / `index.add` / `index.remove` steps. Step order is
1404
+ * deterministic: every `table.remove`, then every `table.add`, then each
1405
+ * shared table's column/index changes in `declared` order. `from` / `to` are
1406
+ * plan labels only — version tracking itself is deferred to persistent
1407
+ * backends.
1408
+ *
1409
+ * @param deployed - The table schemas currently applied
1410
+ * @param declared - The table schemas the caller wants applied
1411
+ * @param from - The plan's source version label (defaults to `0`)
1412
+ * @param to - The plan's target version label (defaults to `1`)
1413
+ * @returns The migration plan moving `deployed` toward `declared`
1414
+ *
1415
+ * @example
1416
+ * ```ts
1417
+ * const plan = planMigration(
1418
+ * [{ name: 'users', primary: 'id', columns: [], indexes: [] }],
1419
+ * [{ name: 'users', primary: 'id', columns: [{ name: 'age', type: 'integer', nullable: false }], indexes: [] }],
1420
+ * )
1421
+ * // plan.steps === [{ operation: 'column.add', table: 'users', column: { name: 'age', ... } }]
1422
+ * ```
1423
+ */
1424
+ function planMigration(deployed, declared, from = 0, to = 1) {
1425
+ const deployedByName = new Map(deployed.map((table) => [table.name, table]));
1426
+ const declaredByName = new Map(declared.map((table) => [table.name, table]));
1427
+ const steps = [];
1428
+ for (const table of deployed) if (!declaredByName.has(table.name)) steps.push({
1429
+ operation: "table.remove",
1430
+ table: table.name
1431
+ });
1432
+ for (const table of declared) if (!deployedByName.has(table.name)) steps.push({
1433
+ operation: "table.add",
1434
+ table
1435
+ });
1436
+ for (const table of declared) {
1437
+ const before = deployedByName.get(table.name);
1438
+ if (before === void 0) continue;
1439
+ const beforeColumns = new Map(before.columns.map((column) => [column.name, column]));
1440
+ const afterColumns = new Map(table.columns.map((column) => [column.name, column]));
1441
+ for (const column of before.columns) if (!afterColumns.has(column.name)) steps.push({
1442
+ operation: "column.remove",
1443
+ table: table.name,
1444
+ column: column.name
1445
+ });
1446
+ for (const column of table.columns) if (!beforeColumns.has(column.name)) steps.push({
1447
+ operation: "column.add",
1448
+ table: table.name,
1449
+ column
1450
+ });
1451
+ const sameIndex = (left, right) => left.length === right.length && left.every((column, position) => column === right[position]);
1452
+ for (const index of before.indexes) if (!table.indexes.some((candidate) => sameIndex(candidate, index))) steps.push({
1453
+ operation: "index.remove",
1454
+ table: table.name,
1455
+ index
1456
+ });
1457
+ for (const index of table.indexes) if (!before.indexes.some((candidate) => sameIndex(candidate, index))) steps.push({
1458
+ operation: "index.add",
1459
+ table: table.name,
1460
+ index
1461
+ });
1462
+ }
1463
+ return {
1464
+ from,
1465
+ to,
1466
+ steps
1467
+ };
1468
+ }
1469
+ /**
1470
+ * Apply one table's {@link MigrationStep}s to its rows — a pure row transform.
1471
+ *
1472
+ * @remarks
1473
+ * `column.remove` drops that field from every row (a fresh copy — inputs are
1474
+ * never mutated, AGENTS §11); `column.add` leaves rows as-is (an absent field
1475
+ * reads as `undefined`, backfill is application policy). `table.add` /
1476
+ * `table.remove` / `index.add` / `index.remove` are no-ops here (they operate
1477
+ * on storage shape, not row shape). Steps for tables other than the one
1478
+ * `rows` belongs to are ignored — pass only the steps relevant to this table.
1479
+ *
1480
+ * @param rows - The table's current rows
1481
+ * @param steps - The migration steps to apply (typically one table's slice of a {@link Migration})
1482
+ * @returns A new array of transformed rows; `rows` is never mutated
1483
+ *
1484
+ * @example
1485
+ * ```ts
1486
+ * const rows = [{ id: 'a', name: 'Ada', legacy: true }]
1487
+ * migrateRows(rows, [{ operation: 'column.remove', table: 'users', column: 'legacy' }])
1488
+ * // => [{ id: 'a', name: 'Ada' }]
1489
+ * ```
1490
+ */
1491
+ function migrateRows(rows, steps) {
1492
+ const removed = steps.filter((step) => step.operation === "column.remove").map((step) => step.column);
1493
+ if (removed.length === 0) return rows.map((row) => ({ ...row }));
1494
+ return rows.map((row) => {
1495
+ const next = {};
1496
+ for (const key of Object.keys(row)) if (!removed.includes(key)) next[key] = row[key];
1497
+ return next;
1498
+ });
1499
+ }
1500
+ /**
1501
+ * Run the driver-conformance battery against a fresh {@link DriverInterface}
1502
+ * per phase, yielding one {@link ConformanceFinding} per violated invariant —
1503
+ * the shared invariant suite every backend (in-memory, SQLite, IndexedDB)
1504
+ * must uphold to be a drop-in {@link DriverInterface}.
1505
+ *
1506
+ * @remarks
1507
+ * Framework-agnostic: no test-runner or Node imports, only sibling core
1508
+ * modules — so it runs equally from a unit test, a smoke script, or a new
1509
+ * driver's own README. Opens a fixed two-table schema (`users` keyed by the
1510
+ * default `id`, `posts` keyed by a non-id `slug`) and, calling `factory()`
1511
+ * fresh for each phase so failures stay isolated, verifies: `open`/`close`;
1512
+ * `read` of a missing key returns `undefined`; `write`/`read` round-trip with
1513
+ * copy-in/copy-out isolation (mutating the caller's row after `write`, or the
1514
+ * row `read` returns, never perturbs stored state) and upsert-overwrite;
1515
+ * `delete` returns `true` then `false`; `keys`/`scan` yield in ascending key
1516
+ * order; `clear` empties only its target table; `snapshot`'s rollback thunk
1517
+ * restores pre-snapshot state; a scoped `snapshot(['users'])` rolls back only
1518
+ * the named table, leaving a concurrent mutation to another table intact; a
1519
+ * non-`id` primary key (`posts.slug`) round-trips; a nested-object row
1520
+ * round-trips structurally (via {@link deepEqual}). The optional surface is
1521
+ * presence-gated: when `migrate` exists, a `column.remove` plan strips the
1522
+ * column from stored rows and a plan referencing an unknown table throws
1523
+ * `DatabaseError` `MIGRATION`; when `stream` exists, it yields only
1524
+ * condition-matching rows and honors `offset`/`limit`; when `transaction`
1525
+ * exists, `commit` persists and `rollback` restores; when both `meta` and
1526
+ * `stamp` exist, a fresh store's `meta()` is `undefined`, and after
1527
+ * `stamp({ version, schema })`, `meta()` returns the exact stamped value.
1528
+ *
1529
+ * Each phase runs within a `try`/`catch`: an EXPECTED mismatch yields a
1530
+ * finding built from the assertion, while an UNEXPECTED throw (a driver
1531
+ * crash mid-phase) is caught and yielded as a finding too, naming the phase
1532
+ * as `check` and carrying the caught error in `context.error` — a broken
1533
+ * driver can never escape the battery as an unhandled rejection. Within a
1534
+ * phase, the FIRST violated assertion yields and the phase stops (matching
1535
+ * the historical fail-fast shape at phase granularity); the generator then
1536
+ * moves on to the next phase regardless. Because this is a **generator**,
1537
+ * consuming only the first yielded value reproduces true fail-fast (later
1538
+ * phases never run) — that is exactly what {@link conformDriver} does.
1539
+ *
1540
+ * @param factory - Mints a fresh, unopened driver instance (called once per phase)
1541
+ * @yields One {@link ConformanceFinding} per violated invariant, in phase order
1542
+ *
1543
+ * @example
1544
+ * ```ts
1545
+ * import { createMemoryDriver, driverFindings } from '@orkestrel/database'
1546
+ *
1547
+ * for await (const finding of driverFindings(() => createMemoryDriver())) {
1548
+ * console.log(finding.check, finding.message)
1549
+ * }
1550
+ * ```
1551
+ */
1552
+ async function* driverFindings(factory) {
1553
+ const CONFORMANCE_USERS_SCHEMA = {
1554
+ name: "users",
1555
+ primary: "id",
1556
+ columns: [
1557
+ {
1558
+ name: "id",
1559
+ type: "text",
1560
+ nullable: false
1561
+ },
1562
+ {
1563
+ name: "name",
1564
+ type: "text",
1565
+ nullable: false
1566
+ },
1567
+ {
1568
+ name: "age",
1569
+ type: "integer",
1570
+ nullable: true
1571
+ }
1572
+ ],
1573
+ indexes: []
1574
+ };
1575
+ const CONFORMANCE_SCHEMA = [CONFORMANCE_USERS_SCHEMA, {
1576
+ name: "posts",
1577
+ primary: "slug",
1578
+ columns: [{
1579
+ name: "slug",
1580
+ type: "text",
1581
+ nullable: false
1582
+ }, {
1583
+ name: "title",
1584
+ type: "text",
1585
+ nullable: false
1586
+ }],
1587
+ indexes: []
1588
+ }];
1589
+ const findingOf = (check, message, context) => ({
1590
+ check,
1591
+ message,
1592
+ context
1593
+ });
1594
+ const phases = [
1595
+ {
1596
+ check: "open-close",
1597
+ run: async () => {
1598
+ const driver = factory();
1599
+ await driver.open(CONFORMANCE_SCHEMA);
1600
+ await driver.close();
1601
+ }
1602
+ },
1603
+ {
1604
+ check: "read-missing",
1605
+ run: async () => {
1606
+ const driver = factory();
1607
+ await driver.open(CONFORMANCE_SCHEMA);
1608
+ const missing = await driver.read("users", "nope");
1609
+ await driver.close();
1610
+ if (missing !== void 0) return findingOf("read-missing", "read of a missing key must return undefined", {
1611
+ table: "users",
1612
+ expected: void 0,
1613
+ actual: missing
1614
+ });
1615
+ }
1616
+ },
1617
+ {
1618
+ check: "write-read",
1619
+ run: async () => {
1620
+ const driver = factory();
1621
+ await driver.open(CONFORMANCE_SCHEMA);
1622
+ const input = {
1623
+ id: "u1",
1624
+ name: "Ada",
1625
+ age: 30
1626
+ };
1627
+ await driver.write("users", "u1", input);
1628
+ input.name = "Mutated after write";
1629
+ const stored = await driver.read("users", "u1");
1630
+ const original = {
1631
+ id: "u1",
1632
+ name: "Ada",
1633
+ age: 30
1634
+ };
1635
+ if (stored === void 0 || !deepEqual(stored, original)) {
1636
+ await driver.close();
1637
+ return findingOf("copy-in", "write must copy the input row rather than store it by reference", {
1638
+ table: "users",
1639
+ expected: original,
1640
+ actual: stored
1641
+ });
1642
+ }
1643
+ stored.name = "Mutated after read";
1644
+ const reread = await driver.read("users", "u1");
1645
+ if (reread === void 0 || !deepEqual(reread, original)) {
1646
+ await driver.close();
1647
+ return findingOf("copy-out", "read must copy the stored row rather than return it by reference", {
1648
+ table: "users",
1649
+ expected: original,
1650
+ actual: reread
1651
+ });
1652
+ }
1653
+ const overwrite = {
1654
+ id: "u1",
1655
+ name: "Ada Overwritten",
1656
+ age: 31
1657
+ };
1658
+ await driver.write("users", "u1", overwrite);
1659
+ const overwritten = await driver.read("users", "u1");
1660
+ await driver.close();
1661
+ if (overwritten === void 0 || !deepEqual(overwritten, overwrite)) return findingOf("upsert", "write must upsert-overwrite an existing key", {
1662
+ table: "users",
1663
+ expected: overwrite,
1664
+ actual: overwritten
1665
+ });
1666
+ }
1667
+ },
1668
+ {
1669
+ check: "delete",
1670
+ run: async () => {
1671
+ const driver = factory();
1672
+ await driver.open(CONFORMANCE_SCHEMA);
1673
+ await driver.write("users", "u1", {
1674
+ id: "u1",
1675
+ name: "Ada",
1676
+ age: 30
1677
+ });
1678
+ const first = await driver.delete("users", "u1");
1679
+ if (first !== true) {
1680
+ await driver.close();
1681
+ return findingOf("delete-true", "delete of an existing key must return true", {
1682
+ table: "users",
1683
+ expected: true,
1684
+ actual: first
1685
+ });
1686
+ }
1687
+ const second = await driver.delete("users", "u1");
1688
+ await driver.close();
1689
+ if (second !== false) return findingOf("delete-false", "delete of an already-removed key must return false", {
1690
+ table: "users",
1691
+ expected: false,
1692
+ actual: second
1693
+ });
1694
+ }
1695
+ },
1696
+ {
1697
+ check: "order",
1698
+ run: async () => {
1699
+ const driver = factory();
1700
+ await driver.open(CONFORMANCE_SCHEMA);
1701
+ for (const row of [
1702
+ {
1703
+ id: "c",
1704
+ name: "C",
1705
+ age: 3
1706
+ },
1707
+ {
1708
+ id: "a",
1709
+ name: "A",
1710
+ age: 1
1711
+ },
1712
+ {
1713
+ id: "b",
1714
+ name: "B",
1715
+ age: 2
1716
+ }
1717
+ ]) await driver.write("users", row.id, row);
1718
+ const expected = [
1719
+ "a",
1720
+ "b",
1721
+ "c"
1722
+ ];
1723
+ const keys = [...await driver.keys("users")];
1724
+ if (!deepEqual(keys, expected)) {
1725
+ await driver.close();
1726
+ return findingOf("keys-order", "keys must be returned in ascending key order", {
1727
+ table: "users",
1728
+ expected,
1729
+ actual: keys
1730
+ });
1731
+ }
1732
+ const scanned = [];
1733
+ for await (const row of driver.scan("users")) scanned.push(row);
1734
+ const scannedIds = scanned.map((row) => row.id);
1735
+ await driver.close();
1736
+ if (!deepEqual(scannedIds, expected)) return findingOf("scan-order", "scan must yield rows in ascending key order", {
1737
+ table: "users",
1738
+ expected,
1739
+ actual: scannedIds
1740
+ });
1741
+ }
1742
+ },
1743
+ {
1744
+ check: "clear",
1745
+ run: async () => {
1746
+ const driver = factory();
1747
+ await driver.open(CONFORMANCE_SCHEMA);
1748
+ await driver.write("users", "u1", {
1749
+ id: "u1",
1750
+ name: "Ada",
1751
+ age: 30
1752
+ });
1753
+ await driver.write("posts", "p1", {
1754
+ slug: "p1",
1755
+ title: "Post"
1756
+ });
1757
+ await driver.clear("users");
1758
+ const usersKeys = await driver.keys("users");
1759
+ const postsKeys = await driver.keys("posts");
1760
+ await driver.close();
1761
+ if (usersKeys.length !== 0) return findingOf("clear-target", "clear must empty the targeted table", {
1762
+ table: "users",
1763
+ expected: [],
1764
+ actual: usersKeys
1765
+ });
1766
+ if (postsKeys.length !== 1) return findingOf("clear-other", "clear must not affect other tables", {
1767
+ table: "posts",
1768
+ expected: 1,
1769
+ actual: postsKeys.length
1770
+ });
1771
+ }
1772
+ },
1773
+ {
1774
+ check: "snapshot",
1775
+ run: async () => {
1776
+ const driver = factory();
1777
+ await driver.open(CONFORMANCE_SCHEMA);
1778
+ const original = {
1779
+ id: "u1",
1780
+ name: "Ada",
1781
+ age: 30
1782
+ };
1783
+ await driver.write("users", "u1", original);
1784
+ const rollback = await driver.snapshot();
1785
+ await driver.write("users", "u2", {
1786
+ id: "u2",
1787
+ name: "Grace",
1788
+ age: 40
1789
+ });
1790
+ await driver.delete("users", "u1");
1791
+ await rollback();
1792
+ const keys = [...await driver.keys("users")];
1793
+ if (!deepEqual(keys, ["u1"])) {
1794
+ await driver.close();
1795
+ return findingOf("snapshot-rollback", "snapshot rollback must restore the pre-snapshot key set", {
1796
+ table: "users",
1797
+ expected: ["u1"],
1798
+ actual: keys
1799
+ });
1800
+ }
1801
+ const restored = await driver.read("users", "u1");
1802
+ await driver.close();
1803
+ if (restored === void 0 || !deepEqual(restored, original)) return findingOf("snapshot-rollback-value", "snapshot rollback must restore pre-snapshot row values", {
1804
+ table: "users",
1805
+ expected: original,
1806
+ actual: restored
1807
+ });
1808
+ }
1809
+ },
1810
+ {
1811
+ check: "non-id-primary",
1812
+ run: async () => {
1813
+ const driver = factory();
1814
+ await driver.open(CONFORMANCE_SCHEMA);
1815
+ await driver.write("posts", "hello-world", {
1816
+ slug: "hello-world",
1817
+ title: "Hello"
1818
+ });
1819
+ const post = await driver.read("posts", "hello-world");
1820
+ const key = post === void 0 ? void 0 : extractKey(post, "slug");
1821
+ await driver.close();
1822
+ if (key !== "hello-world") return findingOf("non-id-primary", "a non-id primary key column must round-trip through the store", {
1823
+ table: "posts",
1824
+ expected: "hello-world",
1825
+ actual: key
1826
+ });
1827
+ }
1828
+ },
1829
+ {
1830
+ check: "nested-roundtrip",
1831
+ run: async () => {
1832
+ const driver = factory();
1833
+ await driver.open(CONFORMANCE_SCHEMA);
1834
+ const nested = {
1835
+ id: "u3",
1836
+ name: "Nested",
1837
+ age: 20,
1838
+ meta: {
1839
+ tags: ["a", "b"],
1840
+ deep: { flag: true }
1841
+ }
1842
+ };
1843
+ await driver.write("users", "u3", nested);
1844
+ const readBack = await driver.read("users", "u3");
1845
+ await driver.close();
1846
+ if (readBack === void 0 || !deepEqual(readBack, nested)) return findingOf("nested-roundtrip", "a nested-object row must round-trip structurally", {
1847
+ table: "users",
1848
+ expected: nested,
1849
+ actual: readBack
1850
+ });
1851
+ }
1852
+ },
1853
+ {
1854
+ check: "migrate",
1855
+ run: async () => {
1856
+ const driver = factory();
1857
+ if (driver.migrate === void 0) return void 0;
1858
+ await driver.open(CONFORMANCE_SCHEMA);
1859
+ await driver.write("users", "u1", {
1860
+ id: "u1",
1861
+ name: "Ada",
1862
+ age: 30,
1863
+ legacy: true
1864
+ });
1865
+ const removePlan = planMigration([{
1866
+ ...CONFORMANCE_USERS_SCHEMA,
1867
+ columns: [...CONFORMANCE_USERS_SCHEMA.columns, {
1868
+ name: "legacy",
1869
+ type: "boolean",
1870
+ nullable: false
1871
+ }]
1872
+ }], [CONFORMANCE_USERS_SCHEMA]);
1873
+ await driver.migrate(removePlan);
1874
+ const migrated = await driver.read("users", "u1");
1875
+ if (migrated === void 0 || "legacy" in migrated) {
1876
+ await driver.close();
1877
+ return findingOf("migrate-column-remove", "a column.remove migration must strip the column from stored rows", {
1878
+ table: "users",
1879
+ expected: void 0,
1880
+ actual: migrated === void 0 ? void 0 : migrated.legacy
1881
+ });
1882
+ }
1883
+ let caught;
1884
+ try {
1885
+ await driver.migrate({
1886
+ from: 0,
1887
+ to: 1,
1888
+ steps: [{
1889
+ operation: "table.remove",
1890
+ table: "ghost"
1891
+ }]
1892
+ });
1893
+ } catch (error) {
1894
+ caught = error;
1895
+ }
1896
+ await driver.close();
1897
+ if (!isDatabaseError(caught) || caught.code !== "MIGRATION") return findingOf("migrate-unknown-table", "a migration step referencing an unknown table must throw a MIGRATION DatabaseError", {
1898
+ table: "ghost",
1899
+ expected: "MIGRATION",
1900
+ actual: isDatabaseError(caught) ? caught.code : caught
1901
+ });
1902
+ }
1903
+ },
1904
+ {
1905
+ check: "stream",
1906
+ run: async () => {
1907
+ const driver = factory();
1908
+ if (driver.stream === void 0) return void 0;
1909
+ await driver.open(CONFORMANCE_SCHEMA);
1910
+ for (const row of [
1911
+ {
1912
+ id: "a",
1913
+ name: "A",
1914
+ age: 10
1915
+ },
1916
+ {
1917
+ id: "b",
1918
+ name: "B",
1919
+ age: 20
1920
+ },
1921
+ {
1922
+ id: "c",
1923
+ name: "C",
1924
+ age: 30
1925
+ }
1926
+ ]) await driver.write("users", row.id, row);
1927
+ const criteria = { conditions: [{
1928
+ column: "age",
1929
+ operator: "above",
1930
+ values: [10],
1931
+ connector: "and"
1932
+ }] };
1933
+ const matched = [];
1934
+ for await (const row of driver.stream("users", criteria)) matched.push(row);
1935
+ const matchedIds = matched.map((row) => row.id).sort();
1936
+ if (!deepEqual(matchedIds, ["b", "c"])) {
1937
+ await driver.close();
1938
+ return findingOf("stream-match", "stream must yield only condition-matching rows", {
1939
+ table: "users",
1940
+ expected: ["b", "c"],
1941
+ actual: matchedIds
1942
+ });
1943
+ }
1944
+ const paged = [];
1945
+ for await (const row of driver.stream("users", {
1946
+ offset: 1,
1947
+ limit: 1
1948
+ })) paged.push(row);
1949
+ await driver.close();
1950
+ if (paged.length !== 1) return findingOf("stream-page", "stream must honor offset and limit", {
1951
+ table: "users",
1952
+ expected: 1,
1953
+ actual: paged.length
1954
+ });
1955
+ }
1956
+ },
1957
+ {
1958
+ check: "transaction",
1959
+ run: async () => {
1960
+ const driver = factory();
1961
+ if (driver.transaction === void 0) return void 0;
1962
+ await driver.open(CONFORMANCE_SCHEMA);
1963
+ await driver.write("users", "u1", {
1964
+ id: "u1",
1965
+ name: "Ada",
1966
+ age: 30
1967
+ });
1968
+ const committing = await driver.transaction();
1969
+ await driver.write("users", "u2", {
1970
+ id: "u2",
1971
+ name: "Grace",
1972
+ age: 40
1973
+ });
1974
+ await committing.commit();
1975
+ const afterCommit = [...await driver.keys("users")].sort();
1976
+ if (!deepEqual(afterCommit, ["u1", "u2"])) {
1977
+ await driver.close();
1978
+ return findingOf("transaction-commit", "transaction commit must persist writes made during the scope", {
1979
+ table: "users",
1980
+ expected: ["u1", "u2"],
1981
+ actual: afterCommit
1982
+ });
1983
+ }
1984
+ const rollingBack = await driver.transaction();
1985
+ await driver.write("users", "u3", {
1986
+ id: "u3",
1987
+ name: "Marie",
1988
+ age: 50
1989
+ });
1990
+ await rollingBack.rollback();
1991
+ const afterRollback = [...await driver.keys("users")].sort();
1992
+ await driver.close();
1993
+ if (!deepEqual(afterRollback, ["u1", "u2"])) return findingOf("transaction-rollback", "transaction rollback must restore pre-transaction state", {
1994
+ table: "users",
1995
+ expected: ["u1", "u2"],
1996
+ actual: afterRollback
1997
+ });
1998
+ }
1999
+ },
2000
+ {
2001
+ check: "meta-stamp",
2002
+ run: async () => {
2003
+ const driver = factory();
2004
+ if (driver.meta === void 0 || driver.stamp === void 0) return void 0;
2005
+ await driver.open(CONFORMANCE_SCHEMA);
2006
+ const fresh = await driver.meta();
2007
+ if (fresh !== void 0) {
2008
+ await driver.close();
2009
+ return findingOf("meta-fresh", "a fresh store must report undefined meta", {
2010
+ expected: void 0,
2011
+ actual: fresh
2012
+ });
2013
+ }
2014
+ const stamped = {
2015
+ version: 1,
2016
+ schema: CONFORMANCE_SCHEMA
2017
+ };
2018
+ await driver.stamp(stamped);
2019
+ const read = await driver.meta();
2020
+ await driver.close();
2021
+ if (read === void 0 || !deepEqual(read, stamped)) return findingOf("meta-stamp", "meta() must return exactly the last-stamped value", {
2022
+ expected: stamped,
2023
+ actual: read
2024
+ });
2025
+ }
2026
+ },
2027
+ {
2028
+ check: "snapshot-scoped",
2029
+ run: async () => {
2030
+ const driver = factory();
2031
+ await driver.open(CONFORMANCE_SCHEMA);
2032
+ await driver.write("users", "u1", {
2033
+ id: "u1",
2034
+ name: "Ada",
2035
+ age: 30
2036
+ });
2037
+ await driver.write("posts", "p1", {
2038
+ slug: "p1",
2039
+ title: "Post"
2040
+ });
2041
+ const rollback = await driver.snapshot(["users"]);
2042
+ await driver.write("users", "u2", {
2043
+ id: "u2",
2044
+ name: "Grace",
2045
+ age: 40
2046
+ });
2047
+ await driver.write("posts", "p2", {
2048
+ slug: "p2",
2049
+ title: "Another post"
2050
+ });
2051
+ await rollback();
2052
+ const usersKeys = [...await driver.keys("users")];
2053
+ if (!deepEqual(usersKeys, ["u1"])) {
2054
+ await driver.close();
2055
+ return findingOf("snapshot-scoped-users", "a scoped snapshot must roll back only the named table", {
2056
+ table: "users",
2057
+ expected: ["u1"],
2058
+ actual: usersKeys
2059
+ });
2060
+ }
2061
+ const postsKeys = [...await driver.keys("posts")].sort();
2062
+ await driver.close();
2063
+ if (!deepEqual(postsKeys, ["p1", "p2"])) return findingOf("snapshot-scoped-posts", "a scoped snapshot must leave an unnamed table's mutations intact", {
2064
+ table: "posts",
2065
+ expected: ["p1", "p2"],
2066
+ actual: postsKeys
2067
+ });
2068
+ }
2069
+ }
2070
+ ];
2071
+ for (const phase of phases) try {
2072
+ const finding = await phase.run();
2073
+ if (finding !== void 0) yield finding;
2074
+ } catch (error) {
2075
+ yield findingOf(phase.check, error instanceof Error ? error.message : String(error), { error });
2076
+ }
2077
+ }
2078
+ /**
2079
+ * Run the driver-conformance battery, throwing on the first violated
2080
+ * invariant — the fail-fast entry point most callers (test setup, CI smoke
2081
+ * checks) want.
2082
+ *
2083
+ * @remarks
2084
+ * A thin driver over {@link driverFindings}: because that generator is
2085
+ * lazy, consuming only its first yielded value means every LATER phase
2086
+ * never runs — true fail-fast, not merely "report only the first". The
2087
+ * thrown error is byte-compatible with the historical shape: a
2088
+ * `CONFORMANCE` {@link DatabaseError} whose `message` is the finding's
2089
+ * `message` and whose `context` is `{ check, ...finding.context }`.
2090
+ *
2091
+ * @param factory - Mints a fresh, unopened driver instance (called once per phase)
2092
+ * @returns Nothing — resolves once every phase has passed
2093
+ * @throws A `CONFORMANCE` {@link DatabaseError} on the first violated invariant
2094
+ *
2095
+ * @example
2096
+ * ```ts
2097
+ * import { conformDriver, createMemoryDriver } from '@orkestrel/database'
2098
+ *
2099
+ * await conformDriver(() => createMemoryDriver()) // resolves when every invariant holds
2100
+ * ```
2101
+ */
2102
+ async function conformDriver(factory) {
2103
+ for await (const finding of driverFindings(factory)) throw new DatabaseError("CONFORMANCE", finding.message, {
2104
+ check: finding.check,
2105
+ ...finding.context
2106
+ });
2107
+ }
2108
+ /**
2109
+ * Run the FULL driver-conformance battery and collect every violation — the
2110
+ * audit entry point for a driver author who wants a complete report rather
2111
+ * than a single fail-fast throw.
2112
+ *
2113
+ * @remarks
2114
+ * Drains {@link driverFindings} to completion: every phase runs regardless
2115
+ * of earlier violations, so a driver breaking two independent invariants
2116
+ * reports both. An empty array means the driver is fully conformant.
2117
+ *
2118
+ * @param factory - Mints a fresh, unopened driver instance (called once per phase)
2119
+ * @returns Every violated invariant found, in phase order (empty when fully conformant)
2120
+ *
2121
+ * @example
2122
+ * ```ts
2123
+ * import { auditDriver, createMemoryDriver } from '@orkestrel/database'
2124
+ *
2125
+ * const findings = await auditDriver(() => createMemoryDriver())
2126
+ * for (const finding of findings) console.log(`${finding.check}: ${finding.message}`)
2127
+ * ```
2128
+ */
2129
+ async function auditDriver(factory) {
2130
+ const findings = [];
2131
+ for await (const finding of driverFindings(factory)) findings.push(finding);
2132
+ return findings;
2133
+ }
2134
+ //#endregion
2135
+ //#region node_modules/@orkestrel/emitter/dist/src/core/index.js
2136
+ /**
2137
+ * Extract the own enumerable keys of a mapped object, typed as its key union.
2138
+ *
2139
+ * @remarks
2140
+ * `Object.keys` widens its result to `string[]`, which breaks the key↔value
2141
+ * correlation a mapped type (like `EmitterHooks<TMap>`) otherwise guarantees.
2142
+ * A `for…in` push into a `keyof`-typed array narrows the result back,
2143
+ * type-safely and with no assertion.
2144
+ *
2145
+ * @typeParam T - The object shape whose keys are extracted.
2146
+ * @param object - The object to read keys from.
2147
+ * @returns The object's own enumerable keys, typed as `(keyof T)[]`.
2148
+ *
2149
+ * @example
2150
+ * ```ts
2151
+ * import { extractKeys } from '@src/core'
2152
+ *
2153
+ * const hooks = { tick: () => {}, done: () => {} }
2154
+ * extractKeys(hooks) // ['tick', 'done']
2155
+ * extractKeys({}) // []
2156
+ * ```
2157
+ */
2158
+ function extractKeys(object) {
2159
+ const collected = [];
2160
+ for (const key in object) collected.push(key);
2161
+ return collected;
2162
+ }
2163
+ Object.freeze([
2164
+ "null",
2165
+ "boolean",
2166
+ "object",
2167
+ "array",
2168
+ "number",
2169
+ "integer",
2170
+ "string"
2171
+ ]);
2172
+ /** Determine whether a value is callable. */
2173
+ function isFunction(value) {
2174
+ return typeof value === "function";
2175
+ }
2176
+ /**
2177
+ * A typed synchronous event emitter — the foundational observable primitive of
2178
+ * the codebase (AGENTS §13). Stateful entities OWN one as a `#emitter` field and
2179
+ * expose it through `readonly emitter`; they never inherit from it.
2180
+ *
2181
+ * @typeParam TMap - The event map: each event name to the argument tuple its
2182
+ * listeners receive.
2183
+ *
2184
+ * @remarks
2185
+ * - **Synchronous.** `emit` invokes listeners in registration order, in the
2186
+ * current tick.
2187
+ * - **Listener isolation.** A throwing listener never stops its siblings: every
2188
+ * listener runs, and a throw is routed to the `error` handler
2189
+ * ({@link EmitterOptions.error}) — never rethrown. Every throwing listener
2190
+ * surfaces (not just the first), and with no `error` handler a throw is swallowed
2191
+ * silently. The `error` handler runs inside its own try/catch, so a throwing
2192
+ * error-handler is swallowed too (anti-recursion — it cannot escape or re-enter).
2193
+ * - **Per-event storage.** Listeners live in a per-event `Set`, so every public
2194
+ * method is precisely typed with no assertions.
2195
+ * - **Destroyed → no-op.** After `destroy()`, `on` / `once` / `emit` do nothing
2196
+ * and `destroyed` is `true`.
2197
+ *
2198
+ * @example
2199
+ * ```ts
2200
+ * type CounterEventMap = {
2201
+ * tick: readonly [count: number]
2202
+ * done: readonly []
2203
+ * }
2204
+ *
2205
+ * const emitter = new Emitter<CounterEventMap>({
2206
+ * on: { done: () => stop() },
2207
+ * error: (error, event) => log(`listener for ${event} threw`, error),
2208
+ * })
2209
+ * emitter.on('tick', (count) => render(count))
2210
+ * emitter.emit('tick', 1)
2211
+ * ```
2212
+ */
2213
+ var Emitter = class {
2214
+ #destroyed = false;
2215
+ #listeners = {};
2216
+ #wrappers = {};
2217
+ #error;
2218
+ constructor(options) {
2219
+ const error = options?.error;
2220
+ this.#error = isFunction(error) ? error : void 0;
2221
+ const hooks = options?.on;
2222
+ if (hooks !== void 0) this.#wire(hooks);
2223
+ }
2224
+ get destroyed() {
2225
+ return this.#destroyed;
2226
+ }
2227
+ on(event, handler) {
2228
+ if (this.#destroyed) return;
2229
+ (this.#listeners[event] ??= /* @__PURE__ */ new Set()).add(handler);
2230
+ }
2231
+ once(event, handler) {
2232
+ if (this.#destroyed) return;
2233
+ const pending = this.#wrappers[event] ??= /* @__PURE__ */ new Map();
2234
+ const wrapper = (...args) => {
2235
+ this.#listeners[event]?.delete(wrapper);
2236
+ const wrappers = pending.get(handler);
2237
+ wrappers?.delete(wrapper);
2238
+ if (wrappers !== void 0 && wrappers.size === 0) pending.delete(handler);
2239
+ handler(...args);
2240
+ };
2241
+ const wrappers = pending.get(handler) ?? /* @__PURE__ */ new Set();
2242
+ wrappers.add(wrapper);
2243
+ pending.set(handler, wrappers);
2244
+ this.on(event, wrapper);
2245
+ }
2246
+ off(event, handler) {
2247
+ const listeners = this.#listeners[event];
2248
+ const wrappers = this.#wrappers[event];
2249
+ const pending = wrappers?.get(handler);
2250
+ if (pending !== void 0) {
2251
+ for (const wrapper of pending) listeners?.delete(wrapper);
2252
+ wrappers?.delete(handler);
2253
+ }
2254
+ listeners?.delete(handler);
2255
+ }
2256
+ emit(event, ...args) {
2257
+ if (this.#destroyed) return;
2258
+ const listeners = this.#listeners[event];
2259
+ if (listeners === void 0) return;
2260
+ for (const handler of [...listeners]) try {
2261
+ handler(...args);
2262
+ } catch (error) {
2263
+ this.#surface(error, event);
2264
+ }
2265
+ }
2266
+ count(event) {
2267
+ if (event !== void 0) return this.#listeners[event]?.size ?? 0;
2268
+ let total = 0;
2269
+ for (const set of Object.values(this.#listeners)) total += set?.size ?? 0;
2270
+ return total;
2271
+ }
2272
+ clear(event) {
2273
+ if (event !== void 0) {
2274
+ delete this.#listeners[event];
2275
+ delete this.#wrappers[event];
2276
+ return;
2277
+ }
2278
+ this.#listeners = {};
2279
+ this.#wrappers = {};
2280
+ }
2281
+ destroy() {
2282
+ this.#listeners = {};
2283
+ this.#wrappers = {};
2284
+ this.#error = void 0;
2285
+ this.#destroyed = true;
2286
+ }
2287
+ #surface(error, event) {
2288
+ const handler = this.#error;
2289
+ if (handler === void 0) return;
2290
+ try {
2291
+ handler(error, String(event));
2292
+ } catch {}
2293
+ }
2294
+ #wire(hooks) {
2295
+ for (const event of extractKeys(hooks)) {
2296
+ const handler = hooks[event];
2297
+ if (isFunction(handler)) this.on(event, handler);
2298
+ }
2299
+ }
2300
+ };
2301
+ //#endregion
2302
+ //#region src/core/Cursor.ts
2303
+ /**
2304
+ * A forward row cursor for bulk in-place mutation.
2305
+ *
2306
+ * @remarks
2307
+ * Iterates a snapshot of the table's keys captured when the cursor was opened,
2308
+ * reading each row lazily through the owning table — so a mutation made during
2309
+ * iteration cannot corrupt the walk, and a key removed mid-iteration is simply
2310
+ * skipped. `update` and `remove` act on the row at the current position.
2311
+ */
2312
+ var Cursor = class {
2313
+ #table;
2314
+ #keys;
2315
+ #index = -1;
2316
+ #value;
2317
+ #closed = false;
2318
+ constructor(table, keys) {
2319
+ this.#table = table;
2320
+ this.#keys = keys;
2321
+ }
2322
+ get value() {
2323
+ return this.#value;
2324
+ }
2325
+ get index() {
2326
+ return this.#index;
2327
+ }
2328
+ get done() {
2329
+ return this.#closed || this.#index >= this.#keys.length;
2330
+ }
2331
+ async next() {
2332
+ if (this.#closed) return;
2333
+ this.#index += 1;
2334
+ while (this.#index < this.#keys.length) {
2335
+ const row = await this.#table.get(this.#keys[this.#index]);
2336
+ if (row !== void 0) {
2337
+ this.#value = row;
2338
+ return;
2339
+ }
2340
+ this.#index += 1;
2341
+ }
2342
+ this.#value = void 0;
2343
+ }
2344
+ async update(changes) {
2345
+ if (this.#closed || this.#value === void 0) return;
2346
+ const key = this.#keys[this.#index];
2347
+ await this.#table.update(key, changes);
2348
+ this.#value = await this.#table.get(key);
2349
+ }
2350
+ async remove() {
2351
+ if (this.#closed || this.#value === void 0) return;
2352
+ await this.#table.remove(this.#keys[this.#index]);
2353
+ this.#value = void 0;
2354
+ }
2355
+ close() {
2356
+ this.#closed = true;
2357
+ this.#value = void 0;
2358
+ }
2359
+ };
2360
+ //#endregion
2361
+ //#region src/core/Clause.ts
2362
+ /**
2363
+ * A pending condition opened by a query's `where` / `and` / `or`.
2364
+ *
2365
+ * @remarks
2366
+ * Holds the column, the connector that will join this condition to the ones
2367
+ * before it, and a recorder the owning query supplies. Each operator builds the
2368
+ * {@link Condition}, hands it to the recorder, and returns the query — so the
2369
+ * fluent chain flows straight back into the builder without exposing a mutator.
2370
+ */
2371
+ var Clause = class {
2372
+ #record;
2373
+ #column;
2374
+ #connector;
2375
+ constructor(record, column, connector) {
2376
+ this.#record = record;
2377
+ this.#column = column;
2378
+ this.#connector = connector;
2379
+ }
2380
+ equals(value) {
2381
+ return this.#apply("equals", [value]);
2382
+ }
2383
+ not(value) {
2384
+ return this.#apply("not", [value]);
2385
+ }
2386
+ above(value) {
2387
+ return this.#apply("above", [value]);
2388
+ }
2389
+ below(value) {
2390
+ return this.#apply("below", [value]);
2391
+ }
2392
+ from(value) {
2393
+ return this.#apply("from", [value]);
2394
+ }
2395
+ to(value) {
2396
+ return this.#apply("to", [value]);
2397
+ }
2398
+ between(lower, upper) {
2399
+ return this.#apply("between", [lower, upper]);
2400
+ }
2401
+ like(pattern) {
2402
+ return this.#apply("like", [pattern]);
2403
+ }
2404
+ glob(pattern) {
2405
+ return this.#apply("glob", [pattern]);
2406
+ }
2407
+ starts(prefix) {
2408
+ return this.#apply("starts", [prefix]);
2409
+ }
2410
+ ends(suffix) {
2411
+ return this.#apply("ends", [suffix]);
2412
+ }
2413
+ any(values) {
2414
+ return this.#apply("any", values);
2415
+ }
2416
+ none(values) {
2417
+ return this.#apply("none", values);
2418
+ }
2419
+ absent() {
2420
+ return this.#apply("absent", []);
2421
+ }
2422
+ present() {
2423
+ return this.#apply("present", []);
2424
+ }
2425
+ #apply(operator, values) {
2426
+ return this.#record({
2427
+ column: this.#column,
2428
+ operator,
2429
+ values,
2430
+ connector: this.#connector
2431
+ });
2432
+ }
2433
+ };
2434
+ //#endregion
2435
+ //#region src/core/Query.ts
2436
+ /**
2437
+ * A fluent query builder bound to one table.
2438
+ *
2439
+ * @remarks
2440
+ * Accumulates conditions, ordering, JS filters, and a page; each builder method
2441
+ * mutates and returns the same instance, so a chain reads as one statement. The
2442
+ * portable parts (conditions, order, page) compile into a {@link Criteria} the
2443
+ * table resolves; a `filter` predicate is applied in memory after the read and
2444
+ * before paging, so it composes with the rest without a backend ever seeing a
2445
+ * JS callback.
2446
+ */
2447
+ var Query = class {
2448
+ #table;
2449
+ #conditions = [];
2450
+ #orders = [];
2451
+ #filters = [];
2452
+ #limit;
2453
+ #offset;
2454
+ constructor(table) {
2455
+ this.#table = table;
2456
+ }
2457
+ where(column) {
2458
+ return this.#clause(column, "and");
2459
+ }
2460
+ and(column) {
2461
+ return this.#clause(column, "and");
2462
+ }
2463
+ or(column) {
2464
+ return this.#clause(column, "or");
2465
+ }
2466
+ filter(predicate) {
2467
+ this.#filters.push(predicate);
2468
+ return this;
2469
+ }
2470
+ ascending(column) {
2471
+ this.#orders.push({
2472
+ column,
2473
+ direction: "ascending"
2474
+ });
2475
+ return this;
2476
+ }
2477
+ descending(column) {
2478
+ this.#orders.push({
2479
+ column,
2480
+ direction: "descending"
2481
+ });
2482
+ return this;
2483
+ }
2484
+ limit(count) {
2485
+ this.#limit = count;
2486
+ return this;
2487
+ }
2488
+ offset(count) {
2489
+ this.#offset = count;
2490
+ return this;
2491
+ }
2492
+ async all() {
2493
+ if (this.#filters.length === 0) return this.#table.records({
2494
+ conditions: this.#conditions,
2495
+ order: this.#orders,
2496
+ limit: this.#limit,
2497
+ offset: this.#offset
2498
+ });
2499
+ const fetched = await this.#table.records({
2500
+ conditions: this.#conditions,
2501
+ order: this.#orders
2502
+ });
2503
+ return this.#page(this.#filtered(fetched));
2504
+ }
2505
+ async first() {
2506
+ return (await this.all())[0];
2507
+ }
2508
+ async count() {
2509
+ if (this.#filters.length === 0) return this.#table.count({ conditions: this.#conditions });
2510
+ const fetched = await this.#table.records({ conditions: this.#conditions });
2511
+ return this.#filtered(fetched).length;
2512
+ }
2513
+ /**
2514
+ * Lazy per-row evaluation of this query's conditions / filters / offset /
2515
+ * limit.
2516
+ *
2517
+ * @remarks
2518
+ * `order` and its comparators are IGNORED (streaming yields unsorted, as rows
2519
+ * are evaluated one at a time). Same abort semantics as
2520
+ * `TableInterface.scan`: the signal (if any) is checked before each yield,
2521
+ * and breaking out early closes the underlying source.
2522
+ *
2523
+ * @param options - `signal` to cancel the iteration; checked before each yield
2524
+ * @returns An async iterable of matching rows
2525
+ */
2526
+ async *stream(options) {
2527
+ if (this.#filters.length === 0) {
2528
+ yield* this.#table.scan({
2529
+ conditions: this.#conditions,
2530
+ limit: this.#limit,
2531
+ offset: this.#offset
2532
+ }, options);
2533
+ return;
2534
+ }
2535
+ const offset = this.#offset ?? 0;
2536
+ let matched = 0;
2537
+ let yielded = 0;
2538
+ for await (const row of this.#table.scan({ conditions: this.#conditions }, options)) {
2539
+ if (this.#limit !== void 0 && yielded >= this.#limit) break;
2540
+ let matches = true;
2541
+ for (const predicate of this.#filters) if (!predicate(row)) {
2542
+ matches = false;
2543
+ break;
2544
+ }
2545
+ if (!matches) continue;
2546
+ if (matched < offset) {
2547
+ matched += 1;
2548
+ continue;
2549
+ }
2550
+ matched += 1;
2551
+ yielded += 1;
2552
+ yield row;
2553
+ }
2554
+ }
2555
+ aggregate(operation, column) {
2556
+ if (this.#filters.length === 0) return this.#table.aggregate(operation, column, { conditions: this.#conditions });
2557
+ return this.#table.records({ conditions: this.#conditions }).then((fetched) => computeAggregate(this.#filtered(fetched), operation, column));
2558
+ }
2559
+ sum(column) {
2560
+ return this.aggregate("sum", column);
2561
+ }
2562
+ average(column) {
2563
+ return this.aggregate("average", column);
2564
+ }
2565
+ minimum(column) {
2566
+ return this.aggregate("minimum", column);
2567
+ }
2568
+ maximum(column) {
2569
+ return this.aggregate("maximum", column);
2570
+ }
2571
+ #clause(column, connector) {
2572
+ return new Clause((condition) => {
2573
+ this.#conditions.push(condition);
2574
+ return this;
2575
+ }, column, connector);
2576
+ }
2577
+ #filtered(rows) {
2578
+ let result = rows;
2579
+ for (const predicate of this.#filters) result = result.filter(predicate);
2580
+ return result;
2581
+ }
2582
+ #page(rows) {
2583
+ const offset = this.#offset ?? 0;
2584
+ if (offset === 0 && this.#limit === void 0) return rows;
2585
+ return rows.slice(offset, this.#limit === void 0 ? void 0 : offset + this.#limit);
2586
+ }
2587
+ };
2588
+ //#endregion
2589
+ //#region src/core/Table.ts
2590
+ /**
2591
+ * A table — typed keyed CRUD plus fluent query and cursor access over a driver.
2592
+ *
2593
+ * @remarks
2594
+ * The table's contract is the load-bearing piece: writes go through `parse`
2595
+ * (coercing inputs and rejecting rows that don't fit with a `VALIDATION` throw),
2596
+ * reads come back through the contract guard (narrowing a stored {@link Row} to
2597
+ * the table's type — no assertion, AGENTS §1), and `contract` is exposed for
2598
+ * introspection and seeding. The driver only stores and scans; all querying is
2599
+ * the shared core engine in `helpers.ts`.
2600
+ *
2601
+ * @remarks
2602
+ * - **Observable (§13).** The owned {@link emitter} ({@link TableEventMap}) carries the
2603
+ * per-row mutation moments — `write` (set / add / update), `remove`, `clear` — for
2604
+ * fire-and-forget observers (cache invalidation, sync, an audit log), ALONGSIDE the
2605
+ * database-level lifecycle. Events carry the affected KEY only (no value payload, to
2606
+ * keep fan-out lean); reads / queries / counts are not emitted. Every event is emitted
2607
+ * directly, strictly AFTER the driver write / delete / clear completes; the emitter
2608
+ * isolates a listener throw and routes it to its `error` handler (the `error` option),
2609
+ * so a buggy observer can never corrupt a write or perturb a transaction.
2610
+ */
2611
+ var Table = class {
2612
+ #ready;
2613
+ #driver;
2614
+ #name;
2615
+ #key;
2616
+ #contract;
2617
+ #guard;
2618
+ #generate;
2619
+ #emitter;
2620
+ constructor(ready, driver, name, key, contract, generate, on, error) {
2621
+ this.#ready = ready;
2622
+ this.#driver = driver;
2623
+ this.#name = name;
2624
+ this.#key = key;
2625
+ this.#contract = contract;
2626
+ this.#guard = contract.is;
2627
+ this.#generate = generate;
2628
+ this.#emitter = new Emitter({
2629
+ on,
2630
+ error
2631
+ });
2632
+ }
2633
+ get emitter() {
2634
+ return this.#emitter;
2635
+ }
2636
+ get name() {
2637
+ return this.#name;
2638
+ }
2639
+ get primary() {
2640
+ return this.#key;
2641
+ }
2642
+ get contract() {
2643
+ return this.#contract;
2644
+ }
2645
+ async get(keys) {
2646
+ await this.#ready();
2647
+ if (isArray(keys)) return this.#each(keys, (key) => this.#read(key));
2648
+ return this.#read(keys);
2649
+ }
2650
+ async resolve(keys) {
2651
+ await this.#ready();
2652
+ if (isArray(keys)) return this.#each(keys, (key) => this.#resolveOne(key));
2653
+ return this.#resolveOne(keys);
2654
+ }
2655
+ async has(keys) {
2656
+ await this.#ready();
2657
+ if (isArray(keys)) return this.#each(keys, async (key) => await this.#read(key) !== void 0);
2658
+ return await this.#read(keys) !== void 0;
2659
+ }
2660
+ async keys() {
2661
+ await this.#ready();
2662
+ return this.#driver.keys(this.#name);
2663
+ }
2664
+ async records(criteria, options) {
2665
+ checkAbort(options?.signal);
2666
+ await this.#ready();
2667
+ const source = await this.#driver.records?.(this.#name, criteria ?? {}) ?? applyCriteria(await this.#collect(), criteria);
2668
+ const rows = [];
2669
+ for (const row of source) if (this.#guard(row)) rows.push(row);
2670
+ return rows;
2671
+ }
2672
+ async count(criteria, options) {
2673
+ checkAbort(options?.signal);
2674
+ await this.#ready();
2675
+ const conditions = criteria?.conditions;
2676
+ const native = await this.#driver.count?.(this.#name, conditions ? { conditions } : {});
2677
+ if (native !== void 0) return native;
2678
+ return filterRows(await this.#collect(), criteria?.conditions ?? []).length;
2679
+ }
2680
+ async aggregate(operation, column, criteria, options) {
2681
+ checkAbort(options?.signal);
2682
+ await this.#ready();
2683
+ const conditions = criteria?.conditions;
2684
+ const filter = conditions ? { conditions } : {};
2685
+ const native = this.#driver.aggregate?.(this.#name, operation, column, filter);
2686
+ if (native !== void 0) return native;
2687
+ return computeAggregate(await this.#driver.records?.(this.#name, filter) ?? filterRows(await this.#collect(), criteria?.conditions ?? []), operation, column);
2688
+ }
2689
+ /**
2690
+ * Stream the table's rows matching `criteria`, applying offset/limit paging.
2691
+ *
2692
+ * @remarks
2693
+ * `criteria.limit` counts rows that pass BOTH the criteria conditions AND the
2694
+ * table's contract guard (a stored row that fails the guard is skipped and
2695
+ * does not count toward `limit`) — this can differ from {@link records}'s
2696
+ * `limit`, which a driver's optional native `records` hook applies BEFORE
2697
+ * the contract guard runs, when storage holds rows that no longer conform
2698
+ * to the table's contract.
2699
+ *
2700
+ * @param criteria - Optional conditions plus offset/limit paging
2701
+ * @param options - `{ signal }` to abort mid-stream
2702
+ * @returns An async generator of matching, guard-conforming rows
2703
+ */
2704
+ async *scan(criteria, options) {
2705
+ checkAbort(options?.signal);
2706
+ await this.#ready();
2707
+ if (this.#driver.stream !== void 0) {
2708
+ for await (const row of this.#driver.stream(this.#name, criteria ?? {})) {
2709
+ checkAbort(options?.signal);
2710
+ const narrowed = this.#cast(row);
2711
+ if (narrowed !== void 0) yield narrowed;
2712
+ }
2713
+ return;
2714
+ }
2715
+ const conditions = criteria?.conditions;
2716
+ const offset = criteria?.offset ?? 0;
2717
+ const limit = criteria?.limit;
2718
+ let matched = 0;
2719
+ let yielded = 0;
2720
+ for await (const row of this.#driver.scan(this.#name)) {
2721
+ checkAbort(options?.signal);
2722
+ if (limit !== void 0 && yielded >= limit) break;
2723
+ if (conditions !== void 0 && conditions.length > 0 && !matchesCriteria(row, conditions)) continue;
2724
+ if (matched < offset) {
2725
+ matched += 1;
2726
+ continue;
2727
+ }
2728
+ matched += 1;
2729
+ const narrowed = this.#cast(row);
2730
+ if (narrowed !== void 0) {
2731
+ yielded += 1;
2732
+ yield narrowed;
2733
+ }
2734
+ }
2735
+ }
2736
+ async set(rows, options) {
2737
+ checkAbort(options?.signal);
2738
+ await this.#ready();
2739
+ if (isArray(rows)) return this.#each(rows, (row) => this.#put(row, false), options?.signal);
2740
+ return this.#put(rows, false);
2741
+ }
2742
+ async add(rows, options) {
2743
+ checkAbort(options?.signal);
2744
+ await this.#ready();
2745
+ if (isArray(rows)) return this.#each(rows, (row) => this.#put(row, true), options?.signal);
2746
+ return this.#put(rows, true);
2747
+ }
2748
+ async update(keys, changes, options) {
2749
+ checkAbort(options?.signal);
2750
+ await this.#ready();
2751
+ if (isArray(keys)) return this.#each(keys, (key) => this.#updateOne(key, changes), options?.signal);
2752
+ return this.#updateOne(keys, changes);
2753
+ }
2754
+ async remove(keys, options) {
2755
+ checkAbort(options?.signal);
2756
+ await this.#ready();
2757
+ if (isArray(keys)) return this.#each(keys, (key) => this.#delete(key), options?.signal);
2758
+ return this.#delete(keys);
2759
+ }
2760
+ async clear() {
2761
+ await this.#ready();
2762
+ await this.#driver.clear(this.#name);
2763
+ this.#emitter.emit("clear");
2764
+ }
2765
+ query() {
2766
+ return new Query(this);
2767
+ }
2768
+ async cursor() {
2769
+ await this.#ready();
2770
+ const cursor = new Cursor(this, await this.#driver.keys(this.#name));
2771
+ await cursor.next();
2772
+ return cursor;
2773
+ }
2774
+ async #each(items, operation, signal) {
2775
+ const results = [];
2776
+ for (const item of items) {
2777
+ checkAbort(signal);
2778
+ results.push(await operation(item));
2779
+ }
2780
+ return results;
2781
+ }
2782
+ async #read(key) {
2783
+ return this.#cast(await this.#driver.read(this.#name, key));
2784
+ }
2785
+ async #resolveOne(key) {
2786
+ const row = await this.#read(key);
2787
+ if (row === void 0) throw new DatabaseError("NOT_FOUND", `No row '${key}' in table '${this.#name}'`, {
2788
+ table: this.#name,
2789
+ key
2790
+ });
2791
+ return row;
2792
+ }
2793
+ async #put(row, exclusive) {
2794
+ const validated = this.#validate(this.#prepare(row));
2795
+ const key = this.#resolveKey(validated);
2796
+ if (exclusive && await this.#driver.read(this.#name, key) !== void 0) throw new DatabaseError("CONFLICT", `Row '${key}' already exists in table '${this.#name}'`, {
2797
+ table: this.#name,
2798
+ key
2799
+ });
2800
+ await this.#driver.write(this.#name, key, validated);
2801
+ this.#emitter.emit("write", key);
2802
+ return key;
2803
+ }
2804
+ async #updateOne(key, changes) {
2805
+ const existing = await this.#driver.read(this.#name, key);
2806
+ if (existing === void 0) return false;
2807
+ await this.#driver.write(this.#name, key, this.#validate(Object.assign({}, existing, changes)));
2808
+ this.#emitter.emit("write", key);
2809
+ return true;
2810
+ }
2811
+ async #delete(key) {
2812
+ const removed = await this.#driver.delete(this.#name, key);
2813
+ if (removed) this.#emitter.emit("remove", key);
2814
+ return removed;
2815
+ }
2816
+ async #collect() {
2817
+ const rows = [];
2818
+ for await (const row of this.#driver.scan(this.#name)) rows.push(row);
2819
+ return rows;
2820
+ }
2821
+ #prepare(row) {
2822
+ if (!isRecord(row)) throw new DatabaseError("VALIDATION", `Row for table '${this.#name}' is not a record`, { table: this.#name });
2823
+ const prepared = { ...row };
2824
+ if (prepared[this.#key] === void 0) {
2825
+ if (this.#generate === void 0) throw new DatabaseError("VALIDATION", `Row for table '${this.#name}' is missing its key column '${this.#key}' and no key factory was provided`, {
2826
+ table: this.#name,
2827
+ column: this.#key
2828
+ });
2829
+ prepared[this.#key] = this.#generate();
2830
+ }
2831
+ return prepared;
2832
+ }
2833
+ #validate(row) {
2834
+ const parsed = this.#contract.parse(row);
2835
+ if (parsed === void 0 || !isRecord(parsed)) throw new DatabaseError("VALIDATION", `Row failed the '${this.#name}' contract`, {
2836
+ table: this.#name,
2837
+ row
2838
+ });
2839
+ return parsed;
2840
+ }
2841
+ #resolveKey(row) {
2842
+ const key = extractKey(row, this.#key);
2843
+ if (key === void 0) throw new DatabaseError("VALIDATION", `Row has no usable key in column '${this.#key}'`, {
2844
+ table: this.#name,
2845
+ column: this.#key
2846
+ });
2847
+ return key;
2848
+ }
2849
+ #cast(row) {
2850
+ return row !== void 0 && this.#guard(row) ? row : void 0;
2851
+ }
2852
+ };
2853
+ //#endregion
2854
+ //#region src/core/Database.ts
2855
+ /**
2856
+ * A database — the ergonomic entry point over a {@link DriverInterface}.
2857
+ *
2858
+ * @remarks
2859
+ * Owns the driver and a `tables` shape map, connecting the driver lazily on first
2860
+ * use so a freshly created database is immediately usable. `table(name)` returns
2861
+ * a table typed by that table's shape `Infer`. `import` registers more tables and
2862
+ * returns a database re-typed with them over the **same** driver and storage;
2863
+ * `export` emits a portable {@link TableExport} per table. `transaction` snapshots
2864
+ * the driver, runs the scope, and rolls every table back if it throws — an
2865
+ * optimistic model that works uniformly across backends rather than reconciling
2866
+ * SQL's and IndexedDB's incompatible native transactions.
2867
+ *
2868
+ * @remarks
2869
+ * - **Versioned (optional).** When {@link DatabaseOptions.version} is set and the driver
2870
+ * implements both {@link DriverInterface.meta} and {@link DriverInterface.stamp},
2871
+ * `open()` reconciles the driver's persisted {@link DriverMeta} against the declared
2872
+ * version INSIDE the same lazy-connect chain, AFTER the `open` event fires — see
2873
+ * {@link DatabaseOptions.version} for the full reconciliation contract.
2874
+ * - **Observable (§13).** The owned {@link emitter} ({@link DatabaseEventMap}) carries the
2875
+ * connection + transaction lifecycle — `open` / `close` / `transaction` / `commit` /
2876
+ * `rollback` — for fire-and-forget observers, ALONGSIDE each table's per-row events. Every
2877
+ * event is emitted directly, strictly AFTER the relevant transition: `commit` only after
2878
+ * the scope succeeds, `rollback` only after every table is restored. The `rollback` emit
2879
+ * OBSERVES the propagated error — it never swallows it (the original throw propagates
2880
+ * exactly as before). The emitter isolates a listener throw and routes it to its `error`
2881
+ * handler (the `error` option), so observation can never reorder, throw into, or corrupt
2882
+ * the snapshot / commit / rollback flow.
2883
+ */
2884
+ var Database = class Database {
2885
+ #driver;
2886
+ #tables;
2887
+ #keys;
2888
+ #indexes;
2889
+ #name;
2890
+ #generate;
2891
+ #version;
2892
+ #emitter;
2893
+ #status = "idle";
2894
+ #ready;
2895
+ constructor(options) {
2896
+ this.#driver = options.driver;
2897
+ this.#tables = options.tables;
2898
+ this.#keys = options.keys ?? {};
2899
+ this.#indexes = options.indexes ?? {};
2900
+ this.#name = options.name ?? "database";
2901
+ this.#generate = options.key;
2902
+ this.#version = options.version;
2903
+ this.#emitter = new Emitter({
2904
+ on: options.on,
2905
+ error: options.error
2906
+ });
2907
+ }
2908
+ get emitter() {
2909
+ return this.#emitter;
2910
+ }
2911
+ get name() {
2912
+ return this.#name;
2913
+ }
2914
+ get status() {
2915
+ return this.#status;
2916
+ }
2917
+ table(name) {
2918
+ if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
2919
+ return this.#build(name, this.#key(name), createContract(objectShape(this.#tables[name])));
2920
+ }
2921
+ import(tables, keys) {
2922
+ return this.#spawn(tables, {
2923
+ ...this.#keys,
2924
+ ...keys
2925
+ });
2926
+ }
2927
+ export() {
2928
+ const result = {};
2929
+ for (const name of Object.keys(this.#tables)) {
2930
+ const columns = this.#tables[name];
2931
+ result[name] = {
2932
+ key: this.#key(name),
2933
+ columns,
2934
+ schema: compileSchema(objectShape(columns))
2935
+ };
2936
+ }
2937
+ return result;
2938
+ }
2939
+ async open() {
2940
+ await this.#connect();
2941
+ }
2942
+ async close() {
2943
+ this.#status = "closed";
2944
+ this.#ready = void 0;
2945
+ await this.#driver.close();
2946
+ this.#emitter.emit("close");
2947
+ }
2948
+ /**
2949
+ * Run `scope` transactionally: commit its writes on success, roll every table
2950
+ * back if it throws.
2951
+ *
2952
+ * @remarks
2953
+ * When the driver implements the optional native {@link DriverInterface.transaction}
2954
+ * hook, that native `commit` / `rollback` handle drives the transaction; otherwise
2955
+ * the universal snapshot floor (`driver.snapshot()`) runs unchanged. Either path
2956
+ * emits the same `transaction` / `commit` / `rollback` lifecycle (AGENTS §13).
2957
+ * `options.signal` is checked ONCE at entry, before connecting or starting any
2958
+ * transactional work — an already-aborted signal throws `ABORTED` and neither the
2959
+ * native hook nor the snapshot floor is invoked. Nesting is unguarded and
2960
+ * unsupported exactly as before: this is a single-writer model, not reentrant.
2961
+ * On the native path, a `scope` throw rolls back via the native handle; a
2962
+ * native `commit` failure propagates as-is with no rollback attempt — the
2963
+ * engine owns transaction state after a failed COMMIT.
2964
+ *
2965
+ * @param scope - The transactional work to run
2966
+ * @param options - `{ signal }` to abort before the transaction starts
2967
+ * @returns The scope's resolved value
2968
+ * @throws An `ABORTED` {@link DatabaseError} when `options.signal` has already fired
2969
+ */
2970
+ async transaction(scope, options) {
2971
+ checkAbort(options?.signal);
2972
+ await this.#connect();
2973
+ const native = await this.#driver.transaction?.();
2974
+ if (native !== void 0) {
2975
+ this.#emitter.emit("transaction");
2976
+ let value;
2977
+ try {
2978
+ value = await scope();
2979
+ } catch (error) {
2980
+ await native.rollback();
2981
+ this.#emitter.emit("rollback", error);
2982
+ throw error;
2983
+ }
2984
+ await native.commit();
2985
+ this.#emitter.emit("commit");
2986
+ return value;
2987
+ }
2988
+ const rollback = await this.#driver.snapshot();
2989
+ this.#emitter.emit("transaction");
2990
+ try {
2991
+ const value = await scope();
2992
+ this.#emitter.emit("commit");
2993
+ return value;
2994
+ } catch (error) {
2995
+ await rollback();
2996
+ this.#emitter.emit("rollback", error);
2997
+ throw error;
2998
+ }
2999
+ }
3000
+ /**
3001
+ * Diff `deployed` against this database's declared schema and apply the
3002
+ * resulting plan through the driver's optional `migrate` hook.
3003
+ *
3004
+ * @param deployed - The schema currently deployed, as {@link TableSchema}s
3005
+ * @param options - `{ signal }` to abort before the migration starts
3006
+ * @returns The applied {@link Migration} plan
3007
+ * @throws A `MIGRATION` {@link DatabaseError} when the driver does not
3008
+ * implement `migrate`, or when a step references an unknown table
3009
+ * (propagated from the driver)
3010
+ * @throws An `ABORTED` {@link DatabaseError} when `options.signal` has
3011
+ * already fired at entry
3012
+ */
3013
+ async migrate(deployed, options) {
3014
+ checkAbort(options?.signal);
3015
+ await this.#connect();
3016
+ const plan = planMigration(deployed, this.#schema());
3017
+ if (this.#driver.migrate === void 0) throw new DatabaseError("MIGRATION", `Database '${this.#name}' driver does not support migration`, { name: this.#name });
3018
+ await this.#apply(plan);
3019
+ return plan;
3020
+ }
3021
+ #build(name, key, contract) {
3022
+ return new Table(() => this.#connect(), this.#driver, name, key, contract, this.#generate);
3023
+ }
3024
+ #spawn(tables, keys) {
3025
+ return new Database({
3026
+ driver: this.#driver,
3027
+ tables,
3028
+ keys,
3029
+ name: this.#name,
3030
+ ...this.#generate === void 0 ? {} : { key: this.#generate }
3031
+ });
3032
+ }
3033
+ #key(name) {
3034
+ return this.#keys[name] ?? "id";
3035
+ }
3036
+ #schema() {
3037
+ return Object.keys(this.#tables).map((name) => {
3038
+ const columns = this.#tables[name];
3039
+ return {
3040
+ name,
3041
+ primary: this.#key(name),
3042
+ columns: Object.keys(columns).map((column) => {
3043
+ const shape = columns[column];
3044
+ return {
3045
+ name: column,
3046
+ type: shapeToColumnType(shape),
3047
+ nullable: shape.type === "optional" || shape.type === "nullable"
3048
+ };
3049
+ }),
3050
+ indexes: this.#indexes[name] ?? []
3051
+ };
3052
+ });
3053
+ }
3054
+ #connect() {
3055
+ if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
3056
+ if (this.#ready === void 0) this.#ready = this.#driver.open(this.#schema()).then(async () => {
3057
+ if (this.#status === "idle") this.#status = "open";
3058
+ this.#emitter.emit("open");
3059
+ await this.#reconcile();
3060
+ });
3061
+ return this.#ready;
3062
+ }
3063
+ async #reconcile() {
3064
+ if (this.#version === void 0 || this.#driver.meta === void 0) return;
3065
+ const declared = this.#schema();
3066
+ const meta = await this.#driver.meta();
3067
+ if (meta === void 0) {
3068
+ await this.#stamp();
3069
+ return;
3070
+ }
3071
+ if (meta.version > this.#version) throw new DatabaseError("MIGRATION", `Database '${this.#name}' store version ${meta.version} is newer than declared version ${this.#version}`, {
3072
+ name: this.#name,
3073
+ stored: meta.version,
3074
+ declared: this.#version
3075
+ });
3076
+ if (meta.version < this.#version) {
3077
+ const plan = planMigration(meta.schema, declared, meta.version, this.#version);
3078
+ if (plan.steps.length > 0 && this.#driver.migrate === void 0) throw new DatabaseError("MIGRATION", `Database '${this.#name}' driver does not support migration`, {
3079
+ name: this.#name,
3080
+ stored: meta.version,
3081
+ declared: this.#version
3082
+ });
3083
+ await this.#apply(plan);
3084
+ }
3085
+ }
3086
+ async #apply(plan) {
3087
+ const native = await this.#driver.transaction?.();
3088
+ if (native !== void 0) {
3089
+ try {
3090
+ await this.#driver.migrate?.(plan);
3091
+ await this.#stamp();
3092
+ } catch (error) {
3093
+ await native.rollback();
3094
+ throw error;
3095
+ }
3096
+ await native.commit();
3097
+ this.#emitter.emit("migrate", plan);
3098
+ return;
3099
+ }
3100
+ await this.#driver.migrate?.(plan);
3101
+ await this.#stamp();
3102
+ this.#emitter.emit("migrate", plan);
3103
+ }
3104
+ async #stamp() {
3105
+ if (this.#version === void 0 || this.#driver.stamp === void 0) return;
3106
+ const meta = {
3107
+ version: this.#version,
3108
+ schema: this.#schema()
3109
+ };
3110
+ await this.#driver.stamp(meta);
3111
+ }
3112
+ };
3113
+ //#endregion
3114
+ //#region src/core/drivers/MemoryDriver.ts
3115
+ /**
3116
+ * The reference {@link DriverInterface} — nested maps, no I/O.
3117
+ *
3118
+ * @remarks
3119
+ * The in-between made concrete: it runs identically in a browser or on a server,
3120
+ * so it is the storage behind tests, ephemeral caches, and any code that wants
3121
+ * the database API without a persistent backend. Rows are copied in and out so a
3122
+ * caller can never mutate stored state by reference (AGENTS §11), and `snapshot`
3123
+ * clones every table to give transactions an exact rollback point. `scan` and
3124
+ * `keys` yield in key order — sorted by the core {@link compareValues} total
3125
+ * order, the same contract the SQLite (`ORDER BY`) and IndexedDB (key-ordered
3126
+ * reads) backends honor, so an unordered read agrees across every backend rather
3127
+ * than leaking Map insertion order. A persistent backend (IndexedDB, SQLite)
3128
+ * implements the same nine methods over real storage.
3129
+ */
3130
+ var MemoryDriver = class {
3131
+ #tables = /* @__PURE__ */ new Map();
3132
+ #meta;
3133
+ async open(schema) {
3134
+ for (const table of schema) if (!this.#tables.has(table.name)) this.#tables.set(table.name, /* @__PURE__ */ new Map());
3135
+ }
3136
+ async close() {}
3137
+ async read(table, key) {
3138
+ const row = this.#store(table).get(key);
3139
+ return row === void 0 ? void 0 : { ...row };
3140
+ }
3141
+ async write(table, key, row) {
3142
+ this.#store(table).set(key, { ...row });
3143
+ }
3144
+ async delete(table, key) {
3145
+ return this.#store(table).delete(key);
3146
+ }
3147
+ async keys(table) {
3148
+ return this.#ordered(table);
3149
+ }
3150
+ async *scan(table) {
3151
+ const store = this.#store(table);
3152
+ for (const key of this.#ordered(table)) {
3153
+ const row = store.get(key);
3154
+ if (row !== void 0) yield { ...row };
3155
+ }
3156
+ }
3157
+ /**
3158
+ * Natively filtered lazy iteration — the {@link DriverInterface.stream} hook.
3159
+ *
3160
+ * @remarks
3161
+ * Iterates the table's keys in the same key order `scan` and `keys` yield
3162
+ * (sorted by {@link compareValues}), testing each row against
3163
+ * `criteria.conditions` (via {@link matchesCriteria}) before counting it
3164
+ * toward `offset` / `limit`. Both are applied lazily as matches are found —
3165
+ * `offset` matches are skipped without being yielded, and iteration stops the
3166
+ * instant `limit` yields have been produced, so a large table is never fully
3167
+ * walked for a small page. `criteria.order` is IGNORED (the same contract as
3168
+ * `TableInterface.scan` and `QueryInterface.stream`): streaming yields key
3169
+ * order, sorted output is `records()`'s job. Rows yield copy-out (AGENTS
3170
+ * §11), and an unknown table mirrors `scan`'s empty-yield behavior.
3171
+ *
3172
+ * @param table - The table to stream
3173
+ * @param criteria - The filter / offset / limit to apply lazily
3174
+ *
3175
+ * @example
3176
+ * ```ts
3177
+ * for await (const row of driver.stream('users', { conditions, limit: 10 })) {
3178
+ * // one matched row at a time, in key order
3179
+ * }
3180
+ * ```
3181
+ */
3182
+ async *stream(table, criteria) {
3183
+ const store = this.#store(table);
3184
+ const conditions = criteria.conditions;
3185
+ const offset = criteria.offset ?? 0;
3186
+ const limit = criteria.limit;
3187
+ let skipped = 0;
3188
+ let yielded = 0;
3189
+ for (const key of this.#ordered(table)) {
3190
+ if (limit !== void 0 && yielded >= limit) return;
3191
+ const row = store.get(key);
3192
+ if (row === void 0) continue;
3193
+ if (conditions !== void 0 && conditions.length > 0 && !matchesCriteria(row, conditions)) continue;
3194
+ if (skipped < offset) {
3195
+ skipped += 1;
3196
+ continue;
3197
+ }
3198
+ yield { ...row };
3199
+ yielded += 1;
3200
+ }
3201
+ }
3202
+ async clear(table) {
3203
+ this.#store(table).clear();
3204
+ }
3205
+ /**
3206
+ * Capture the current state and return a thunk that rolls back to it.
3207
+ *
3208
+ * @remarks
3209
+ * `tables` omitted clones and restores the WHOLE store, byte-identical to the
3210
+ * prior whole-store behavior. `tables` provided clones ONLY the named tables,
3211
+ * and the returned thunk restores ONLY those — every other table keeps
3212
+ * whatever it was mutated to after the snapshot was taken.
3213
+ *
3214
+ * @param tables - The table names to scope the snapshot to; omitted captures every table
3215
+ * @returns A thunk that restores the captured tables
3216
+ */
3217
+ async snapshot(tables) {
3218
+ if (tables === void 0) {
3219
+ const copy = /* @__PURE__ */ new Map();
3220
+ for (const [name, store] of this.#tables) {
3221
+ const cloned = /* @__PURE__ */ new Map();
3222
+ for (const [key, row] of store) cloned.set(key, { ...row });
3223
+ copy.set(name, cloned);
3224
+ }
3225
+ return async () => {
3226
+ this.#tables.clear();
3227
+ for (const [name, store] of copy) this.#tables.set(name, store);
3228
+ };
3229
+ }
3230
+ const copy = /* @__PURE__ */ new Map();
3231
+ for (const name of tables) {
3232
+ const store = this.#tables.get(name);
3233
+ if (store === void 0) continue;
3234
+ const cloned = /* @__PURE__ */ new Map();
3235
+ for (const [key, row] of store) cloned.set(key, { ...row });
3236
+ copy.set(name, cloned);
3237
+ }
3238
+ return async () => {
3239
+ for (const [name, store] of copy) this.#tables.set(name, store);
3240
+ };
3241
+ }
3242
+ /**
3243
+ * Return the persisted {@link DriverMeta}, or `undefined` when the store has
3244
+ * never been stamped.
3245
+ *
3246
+ * @remarks
3247
+ * In-process only — the metadata lives in this instance's memory, exactly
3248
+ * like the rest of this driver's storage. A driver-conformance-valid
3249
+ * implementation of the optional `meta` / `stamp` pair.
3250
+ *
3251
+ * @returns The last-stamped {@link DriverMeta}, or `undefined`
3252
+ */
3253
+ async meta() {
3254
+ return this.#meta;
3255
+ }
3256
+ /**
3257
+ * Persist `meta` verbatim for a later `meta()` to return.
3258
+ *
3259
+ * @param meta - The {@link DriverMeta} to persist
3260
+ */
3261
+ async stamp(meta) {
3262
+ this.#meta = meta;
3263
+ }
3264
+ /**
3265
+ * Apply a {@link Migration} plan's steps against the in-memory store.
3266
+ *
3267
+ * @remarks
3268
+ * A multi-step plan applies its steps sequentially and is NOT atomic — a
3269
+ * failure partway through a plan leaves the earlier steps already applied.
3270
+ *
3271
+ * @param plan - The migration plan to apply
3272
+ */
3273
+ async migrate(plan) {
3274
+ for (const step of plan.steps) switch (step.operation) {
3275
+ case "table.add":
3276
+ if (!this.#tables.has(step.table.name)) this.#tables.set(step.table.name, /* @__PURE__ */ new Map());
3277
+ break;
3278
+ case "table.remove":
3279
+ this.#require(step.table);
3280
+ this.#tables.delete(step.table);
3281
+ break;
3282
+ case "column.add":
3283
+ case "column.remove": {
3284
+ const store = this.#require(step.table);
3285
+ const rows = [...store.entries()];
3286
+ const migrated = migrateRows(rows.map(([, row]) => row), [step]);
3287
+ rows.forEach(([key], index) => store.set(key, migrated[index]));
3288
+ break;
3289
+ }
3290
+ case "index.add":
3291
+ case "index.remove":
3292
+ this.#require(step.table);
3293
+ break;
3294
+ }
3295
+ }
3296
+ #ordered(table) {
3297
+ return [...this.#store(table).keys()].sort(compareValues);
3298
+ }
3299
+ #require(table) {
3300
+ const store = this.#tables.get(table);
3301
+ if (store === void 0) throw new DatabaseError("MIGRATION", `migrate: unknown table '${table}'`, { table });
3302
+ return store;
3303
+ }
3304
+ #store(table) {
3305
+ let store = this.#tables.get(table);
3306
+ if (store === void 0) {
3307
+ store = /* @__PURE__ */ new Map();
3308
+ this.#tables.set(table, store);
3309
+ }
3310
+ return store;
3311
+ }
3312
+ };
3313
+ //#endregion
3314
+ //#region src/core/factories.ts
3315
+ /**
3316
+ * Create a database over a driver and a declared `tables` schema.
3317
+ *
3318
+ * @remarks
3319
+ * `tables` maps each name to its columns (a `column → shape` map); the database
3320
+ * wraps each in an `objectShape`, so you never write `objectShape` at the table
3321
+ * level. The `const` type parameter captures the literal names and columns, so
3322
+ * `db.table('users')` is checked against the schema and typed by `Infer` of its
3323
+ * columns — no annotations. Name a non-`id` primary-key column per table via the
3324
+ * optional `keys` map.
3325
+ *
3326
+ * @param options - The driver, the `tables` column map, optional `keys`, and an
3327
+ * optional `name`
3328
+ * @returns A typed {@link DatabaseInterface}
3329
+ *
3330
+ * @example
3331
+ * ```ts
3332
+ * import { createDatabase, createMemoryDriver } from '@orkestrel/database'
3333
+ * import { integerShape, stringShape } from '@orkestrel/contract'
3334
+ *
3335
+ * const db = createDatabase({
3336
+ * driver: createMemoryDriver(),
3337
+ * tables: {
3338
+ * users: { id: stringShape(), age: integerShape() },
3339
+ * posts: { slug: stringShape(), title: stringShape() },
3340
+ * },
3341
+ * keys: { posts: 'slug' },
3342
+ * })
3343
+ * await db.table('users').set({ id: 'u1', age: 36 }) // typed; coerced + validated
3344
+ * ```
3345
+ */
3346
+ function createDatabase(options) {
3347
+ return new Database(options);
3348
+ }
3349
+ /**
3350
+ * Create the in-memory reference {@link DriverInterface}.
3351
+ *
3352
+ * @remarks
3353
+ * Backed by nested maps with no I/O — the same driver runs in a browser or on a
3354
+ * server, making it the natural choice for tests and ephemeral storage.
3355
+ *
3356
+ * @returns A fresh in-memory driver
3357
+ */
3358
+ function createMemoryDriver() {
3359
+ return new MemoryDriver();
3360
+ }
3361
+ //#endregion
3362
+ export { Clause, Cursor, DEFAULT_PRIMARY, Database, DatabaseError, MAX_PATTERN_LENGTH, MemoryDriver, Query, Table, applyCriteria, auditDriver, checkAbort, compareValues, computeAggregate, conformDriver, createDatabase, createMemoryDriver, deepEqual, driverFindings, extractKey, filterRows, globMatch, isDatabaseError, likeMatch, matchesCondition, matchesCriteria, migrateRows, planMigration, shapeToColumnType, sortRows, wildcardMatch };
3363
+
3364
+ //# sourceMappingURL=index.js.map