@orkestrel/database 0.0.17 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- import { arrayOf, cloneJSONRecord, cloneJSONValue, compileGuard, compileSchema, createContract, holds, isArray, isBoolean, isError, isFiniteNumber, isInstance, isInteger, isNumber, isRecord, isString, objectShape, parseNumber, resolveField } from "@orkestrel/contract";
1
+ import { INTRINSICS, arrayOf, attempt, cloneJSONRecord, cloneJSONValue, compileGuard, compileSchema, createContract, holds, isArray, isBoolean, isError, isFiniteNumber, isFunction, isInstance, isInteger, isNumber, isObject, isRecord, isString, objectShape, parseNumber, resolveField } from "@orkestrel/contract";
2
2
  import { Emitter } from "@orkestrel/emitter";
3
- //#region src/core/constants.ts
3
+ // src/core/constants.ts
4
4
  /**
5
5
  * Supplies the primary-key column, `'id'`, assumed when {@link PrimaryMap} does not name one.
6
6
  *
@@ -99,8 +99,7 @@ var CONFORMANCE_POSTS_SCHEMA = Object.freeze({
99
99
  * what a later phase opens.
100
100
  */
101
101
  var CONFORMANCE_SCHEMA = Object.freeze([CONFORMANCE_USERS_SCHEMA, CONFORMANCE_POSTS_SCHEMA]);
102
- //#endregion
103
- //#region src/core/errors.ts
102
+ // src/core/errors.ts
104
103
  /**
105
104
  * Represents an error thrown by the database layer.
106
105
  *
@@ -122,6 +121,7 @@ var DatabaseError = class extends Error {
122
121
  context;
123
122
  constructor(code, message, context) {
124
123
  super(message);
124
+ INTRINSICS.define(this, Symbol.for("@orkestrel/database.DatabaseError"), { value: this });
125
125
  this.name = "DatabaseError";
126
126
  this.code = code;
127
127
  if (context !== void 0) this.context = context;
@@ -130,6 +130,11 @@ var DatabaseError = class extends Error {
130
130
  /**
131
131
  * Narrows an unknown caught value to a {@link DatabaseError}.
132
132
  *
133
+ * @remarks
134
+ * Accepts branded instances and subclasses across package copies and module formats.
135
+ * Requires the declared code and context shape. Refuses look-alikes and transparent
136
+ * proxies without requiring an exact `name`.
137
+ *
133
138
  * @param value - The value to test (typically a `catch` binding)
134
139
  * @returns True if `value` is a {@link DatabaseError}; false otherwise
135
140
  *
@@ -143,10 +148,17 @@ var DatabaseError = class extends Error {
143
148
  * ```
144
149
  */
145
150
  function isDatabaseError(value) {
146
- return isInstance(value, DatabaseError);
151
+ return holds(() => {
152
+ if (!isInstance(value, Error) || INTRINSICS.prototype(value) === Error.prototype) return false;
153
+ const brand = INTRINSICS.describe(value, Symbol.for("@orkestrel/database.DatabaseError"));
154
+ if (brand === void 0 || brand.value !== value) return false;
155
+ const code = INTRINSICS.reflect.read(value, "code");
156
+ if (code !== "CLOSED" && code !== "NOT_FOUND" && code !== "CONFLICT" && code !== "VALIDATION" && code !== "ABORTED" && code !== "MIGRATION" && code !== "CONFORMANCE" && code !== "DRIVER") return false;
157
+ const context = INTRINSICS.reflect.read(value, "context");
158
+ return context === void 0 || isObject(context) || isFunction(context);
159
+ });
147
160
  }
148
- //#endregion
149
- //#region src/core/validators.ts
161
+ // src/core/validators.ts
150
162
  /**
151
163
  * Checks whether a value is a usable database key.
152
164
  *
@@ -291,8 +303,7 @@ function isMigrationInput(value) {
291
303
  return (keys.length === 1 || keys.length === 2) && keys.includes("plan") && (keys.length === 1 || keys.includes("metadata")) && isMigration(input.plan) && (input.metadata === void 0 || isDriverMetadata(input.metadata));
292
304
  });
293
305
  }
294
- //#endregion
295
- //#region src/core/cloners.ts
306
+ // src/core/cloners.ts
296
307
  /**
297
308
  * Clones unknown driver metadata into a distinct deeply frozen snapshot.
298
309
  *
@@ -365,8 +376,7 @@ function cloneMigrationInput(value) {
365
376
  });
366
377
  }
367
378
  }
368
- //#endregion
369
- //#region src/core/helpers.ts
379
+ // src/core/helpers.ts
370
380
  /**
371
381
  * Validates the paging fields of a portable query.
372
382
  *
@@ -469,7 +479,7 @@ function equalsValue(left, right) {
469
479
  else prior.add(currentRight);
470
480
  if (leftArray && rightArray) {
471
481
  if (currentLeft.length !== currentRight.length) return false;
472
- for (let index = 0; index < currentLeft.length; index += 1) {
482
+ for (const index of Array.prototype.keys.call(currentLeft)) {
473
483
  const leftOwn = Object.hasOwn(currentLeft, index);
474
484
  if (leftOwn !== Object.hasOwn(currentRight, index)) return false;
475
485
  if (leftOwn) pending.push([currentLeft[index], currentRight[index]]);
@@ -492,6 +502,27 @@ function equalsValue(left, right) {
492
502
  }
493
503
  }
494
504
  /**
505
+ * Checks whether a fixed-length wildcard segment matches a value at an offset.
506
+ *
507
+ * @remarks
508
+ * Compares UTF-16 code units: `single` matches any one unit, and every other segment
509
+ * unit matches itself. An offset that leaves the segment outside the value fails.
510
+ *
511
+ * @param value - The value to read
512
+ * @param segment - The segment, holding no any-run wildcard
513
+ * @param at - The value offset the segment starts at
514
+ * @param single - The single-char wildcard char
515
+ * @returns True if every segment unit matches the value unit at its offset; false otherwise
516
+ *
517
+ * @example
518
+ * ```ts
519
+ * matchesWildcardSegment('hello', 'l_o', 2, '_') // true
520
+ * ```
521
+ */
522
+ function matchesWildcardSegment(value, segment, at, single) {
523
+ return at >= 0 && at + segment.length <= value.length && segment.split("").every((unit, offset) => unit === single || unit === value[at + offset]);
524
+ }
525
+ /**
495
526
  * Matches a value against a wildcard pattern in linear time — the shared, ReDoS-safe
496
527
  * engine behind {@link matchesLikePattern} and {@link matchesGlobPattern}.
497
528
  *
@@ -500,19 +531,20 @@ function equalsValue(left, right) {
500
531
  * `.*` segments separated by literals, matched against a long non-matching input, blow
501
532
  * up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it,
502
533
  * while a `LIKE` / `GLOB` pattern is a caller-supplied operand this package cannot
503
- * trust. So this builds no regex. It runs the classic greedy two-pointer wildcard match:
504
- * the `any` wildcard records its position and, on a later mismatch, backtracks only to
505
- * that last `any` (letting it absorb one more char) — so the work is O(value × pattern),
506
- * never the exponential / polynomial backtracking a regex would do. The pattern length
507
- * is capped at {@link MAX_PATTERN_LENGTH} (a `VALIDATION` {@link DatabaseError} over it),
508
- * bounding the pattern factor so a match stays linear in the value length whatever the
509
- * pattern.
534
+ * trust. This builds no regex. It splits the pattern on `any` into fixed-length
535
+ * segments, anchors the first at the value's start and the last at its end, and places
536
+ * each middle segment at its leftmost match after the one before it, which is the only
537
+ * placement a match needs, so the work is O(value × pattern) at worst, never the
538
+ * exponential / polynomial backtracking a regex would do. The pattern length is capped
539
+ * at {@link MAX_PATTERN_LENGTH} (a `VALIDATION` {@link DatabaseError} over it), bounding
540
+ * the pattern factor so a match stays linear in the value length whatever the pattern.
510
541
  *
511
542
  * The `any` wildcard matches any run (including empty); `single` matches exactly one
512
- * char; every other pattern char matches itself literally (a pattern `.` / `(` / `\` is
513
- * a literal — the regex-metacharacter hazard is gone with the regex). `any` is tested
514
- * before a literal match, so a value that literally contains the wildcard char never
515
- * shadows the wildcard. Case folding is applied to both sides when `fold` is set.
543
+ * UTF-16 code unit; every other pattern char matches itself literally (a pattern `.` /
544
+ * `(` / `\` is a literal — the regex-metacharacter hazard is gone with the regex). The
545
+ * pattern splits on `any` before any literal comparison, so a value that literally
546
+ * contains the wildcard char never shadows the wildcard. Case folding is applied to
547
+ * both sides when `fold` is set.
516
548
  *
517
549
  * @param value - The value to test
518
550
  * @param pattern - The wildcard pattern
@@ -529,27 +561,17 @@ function matchesWildcardPattern(value, pattern, any, single, fold) {
529
561
  });
530
562
  const haystack = fold ? value.toLowerCase() : value;
531
563
  const needle = fold ? pattern.toLowerCase() : pattern;
532
- let vi = 0;
533
- let pi = 0;
534
- let star = -1;
535
- let mark = 0;
536
- while (vi < haystack.length) {
537
- const pc = pi < needle.length ? needle[pi] : void 0;
538
- if (pc === any) {
539
- star = pi;
540
- mark = vi;
541
- pi += 1;
542
- } else if (pc !== void 0 && (pc === single || pc === haystack[vi])) {
543
- vi += 1;
544
- pi += 1;
545
- } else if (star !== -1) {
546
- pi = star + 1;
547
- mark += 1;
548
- vi = mark;
549
- } else return false;
550
- }
551
- while (pi < needle.length && needle[pi] === any) pi += 1;
552
- return pi === needle.length;
564
+ const segments = any.length === 1 ? needle.split(any) : [needle];
565
+ const head = segments[0] ?? "";
566
+ const tail = segments.at(-1) ?? "";
567
+ if (segments.length === 1) return haystack.length === head.length && matchesWildcardSegment(haystack, head, 0, single);
568
+ const end = haystack.length - tail.length;
569
+ if (end < head.length || !matchesWildcardSegment(haystack, head, 0, single) || !matchesWildcardSegment(haystack, tail, end, single)) return false;
570
+ return segments.slice(1, -1).reduce((from, segment) => {
571
+ if (from === void 0) return void 0;
572
+ const at = segment.includes(single) ? new Array(Math.max(0, end - segment.length - from + 1)).keys().map((offset) => from + offset).find((candidate) => matchesWildcardSegment(haystack, segment, candidate, single)) : haystack.indexOf(segment, from);
573
+ return at === void 0 || at < 0 || at + segment.length > end ? void 0 : at + segment.length;
574
+ }, head.length) !== void 0;
553
575
  }
554
576
  /**
555
577
  * Matches a value against a SQL `LIKE` pattern, folding case.
@@ -655,16 +677,10 @@ function matchesCondition(row, condition) {
655
677
  * @returns True if the row satisfies the combined conditions; false otherwise
656
678
  */
657
679
  function matchesQuery(row, conditions) {
658
- let result = true;
659
- let seeded = false;
660
- for (const condition of conditions) {
680
+ return Iterator.from(conditions).reduce((previous, condition) => {
661
681
  const match = matchesCondition(row, condition);
662
- if (!seeded) {
663
- result = match;
664
- seeded = true;
665
- } else result = condition.connector === "or" ? result || match : result && match;
666
- }
667
- return result;
682
+ return previous === void 0 ? match : condition.connector === "or" ? previous || match : previous && match;
683
+ }, void 0) ?? true;
668
684
  }
669
685
  /**
670
686
  * Filters rows by a list of conditions — the shared basis for a table's count
@@ -727,15 +743,13 @@ function sortRows(rows, order) {
727
743
  */
728
744
  function applyQuery(rows, input) {
729
745
  validatePage(input);
730
- let result = rows;
731
746
  const conditions = input?.conditions;
732
- if (conditions !== void 0 && conditions.length > 0) result = result.filter((row) => matchesQuery(row, conditions));
747
+ const filtered = conditions !== void 0 && conditions.length > 0 ? rows.filter((row) => matchesQuery(row, conditions)) : rows;
733
748
  const order = input?.order;
734
- if (order !== void 0 && order.length > 0) result = sortRows(result, order);
749
+ const sorted = order !== void 0 && order.length > 0 ? sortRows(filtered, order) : filtered;
735
750
  const offset = input?.offset ?? 0;
736
751
  const limit = input?.limit;
737
- if (offset > 0 || limit !== void 0) result = result.slice(offset, limit !== void 0 ? offset + limit : void 0);
738
- return result;
752
+ return offset > 0 || limit !== void 0 ? sorted.slice(offset, limit !== void 0 ? offset + limit : void 0) : sorted;
739
753
  }
740
754
  /**
741
755
  * Computes an aggregate over a column across rows.
@@ -753,20 +767,17 @@ function applyQuery(rows, input) {
753
767
  */
754
768
  function computeAggregate(rows, operation, column) {
755
769
  if (operation === "count") return rows.length;
756
- const numbers = [];
757
- for (const row of rows) {
758
- if (!isRecord(row)) continue;
770
+ const numbers = Iterator.from(rows).flatMap((row) => {
771
+ if (!isRecord(row)) return [];
759
772
  const value = parseNumber(resolveField(row, column));
760
- if (value !== void 0) numbers.push(value);
761
- }
773
+ return value === void 0 ? [] : [value];
774
+ }).toArray();
762
775
  if (numbers.length === 0) return void 0;
763
776
  if (operation === "sum" || operation === "average") {
764
777
  const total = numbers.reduce((sum, value) => sum + value, 0);
765
778
  return operation === "average" ? total / numbers.length : total;
766
779
  }
767
- let result = operation === "minimum" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
768
- for (const value of numbers) result = operation === "minimum" ? Math.min(result, value) : Math.max(result, value);
769
- return result;
780
+ return numbers.reduce((result, value) => operation === "minimum" ? Math.min(result, value) : Math.max(result, value), operation === "minimum" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY);
770
781
  }
771
782
  /**
772
783
  * Reads a row's primary key from a column, when it is a usable {@link Key}.
@@ -977,14 +988,12 @@ function planMigration(deployed, declared, from = 0, to = 1) {
977
988
  from,
978
989
  to
979
990
  });
980
- let beforeSchema;
981
- let targetSchema;
982
- try {
983
- beforeSchema = normalizeDriverSchema(deployed);
984
- targetSchema = normalizeDriverSchema(declared);
985
- } catch (error) {
986
- throw new DatabaseError("MIGRATION", "Migration schema is invalid", { cause: error });
987
- }
991
+ const schemas = attempt(() => ({
992
+ before: normalizeDriverSchema(deployed),
993
+ target: normalizeDriverSchema(declared)
994
+ }));
995
+ if (!schemas.success) throw new DatabaseError("MIGRATION", "Migration schema is invalid", { cause: schemas.error });
996
+ const { before: beforeSchema, target: targetSchema } = schemas.value;
988
997
  const deployedByName = new Map(beforeSchema.map((table) => [table.name, table]));
989
998
  const declaredByName = new Map(targetSchema.map((table) => [table.name, table]));
990
999
  const steps = [];
@@ -1070,18 +1079,16 @@ function planMigration(deployed, declared, from = 0, to = 1) {
1070
1079
  * @returns A fresh owned final schema
1071
1080
  */
1072
1081
  function projectMigrationSchema(schema, steps) {
1073
- let owned;
1074
- let projectedSteps;
1075
- try {
1076
- owned = normalizeDriverSchema(schema);
1077
- projectedSteps = cloneMigrationInput({ plan: {
1082
+ const captured = attempt(() => ({
1083
+ owned: normalizeDriverSchema(schema),
1084
+ steps: cloneMigrationInput({ plan: {
1078
1085
  from: 0,
1079
1086
  to: 1,
1080
1087
  steps
1081
- } }).plan.steps;
1082
- } catch (error) {
1083
- throw new DatabaseError("MIGRATION", "Migration input is invalid", { cause: error });
1084
- }
1088
+ } }).plan.steps
1089
+ }));
1090
+ if (!captured.success) throw new DatabaseError("MIGRATION", "Migration input is invalid", { cause: captured.error });
1091
+ const { owned, steps: projectedSteps } = captured.value;
1085
1092
  const tables = new Map(owned.map((table) => [table.name, table]));
1086
1093
  for (const step of projectedSteps) {
1087
1094
  if (step.operation === "table.add") {
@@ -1390,10 +1397,8 @@ async function* scanDriver(factory) {
1390
1397
  name: "Grace",
1391
1398
  age: 40
1392
1399
  })]);
1393
- let fulfilled = 0;
1394
- let conflicted = 0;
1395
- for (const outcome of outcomes) if (outcome.status === "fulfilled") fulfilled += 1;
1396
- else if (isDatabaseError(outcome.reason) && outcome.reason.code === "CONFLICT") conflicted += 1;
1400
+ const fulfilled = outcomes.filter((outcome) => outcome.status === "fulfilled").length;
1401
+ const conflicted = outcomes.filter((outcome) => outcome.status === "rejected" && isDatabaseError(outcome.reason) && outcome.reason.code === "CONFLICT").length;
1397
1402
  const keys = await driver.keys("users");
1398
1403
  await driver.close();
1399
1404
  if (fulfilled !== 1 || conflicted !== 1 || !equalsValue(keys, ["u1"])) yield {
@@ -1469,32 +1474,17 @@ async function* scanDriver(factory) {
1469
1474
  });
1470
1475
  const controller = new AbortController();
1471
1476
  controller.abort("conformance abort");
1472
- let writeError;
1473
- let insertError;
1474
- let deleteError;
1475
- try {
1476
- await driver.write("users", "u2", {
1477
- id: "u2",
1478
- name: "Grace",
1479
- age: 40
1480
- }, { signal: controller.signal });
1481
- } catch (error) {
1482
- writeError = error;
1483
- }
1484
- try {
1485
- await driver.insert("users", "u2", {
1486
- id: "u2",
1487
- name: "Grace",
1488
- age: 40
1489
- }, { signal: controller.signal });
1490
- } catch (error) {
1491
- insertError = error;
1492
- }
1493
- try {
1494
- await driver.delete("users", "u1", { signal: controller.signal });
1495
- } catch (error) {
1496
- deleteError = error;
1497
- }
1477
+ const writeError = await captureFailure(() => driver.write("users", "u2", {
1478
+ id: "u2",
1479
+ name: "Grace",
1480
+ age: 40
1481
+ }, { signal: controller.signal }));
1482
+ const insertError = await captureFailure(() => driver.insert("users", "u2", {
1483
+ id: "u2",
1484
+ name: "Grace",
1485
+ age: 40
1486
+ }, { signal: controller.signal }));
1487
+ const deleteError = await captureFailure(() => driver.delete("users", "u1", { signal: controller.signal }));
1498
1488
  const keys = await driver.keys("users");
1499
1489
  await driver.close();
1500
1490
  if (!isDatabaseError(writeError) || writeError.code !== "ABORTED" || !isDatabaseError(insertError) || insertError.code !== "ABORTED" || !isDatabaseError(deleteError) || deleteError.code !== "ABORTED" || !equalsValue(keys, ["u1"])) yield {
@@ -1804,19 +1794,15 @@ async function* scanDriver(factory) {
1804
1794
  };
1805
1795
  break migratePhase;
1806
1796
  }
1807
- let caught;
1808
- try {
1809
- await driver.migrate({ plan: {
1810
- from: 0,
1811
- to: 1,
1812
- steps: [{
1813
- operation: "table.remove",
1814
- table: "ghost"
1815
- }]
1816
- } });
1817
- } catch (error) {
1818
- caught = error;
1819
- }
1797
+ const migrate = driver.migrate.bind(driver);
1798
+ const caught = await captureFailure(() => migrate({ plan: {
1799
+ from: 0,
1800
+ to: 1,
1801
+ steps: [{
1802
+ operation: "table.remove",
1803
+ table: "ghost"
1804
+ }]
1805
+ } }));
1820
1806
  await driver.close();
1821
1807
  if (!isDatabaseError(caught) || caught.code !== "MIGRATION") yield {
1822
1808
  check: "migrate-unknown-table",
@@ -2113,8 +2099,23 @@ async function auditDriver(factory) {
2113
2099
  for await (const finding of scanDriver(factory)) findings.push(finding);
2114
2100
  return findings;
2115
2101
  }
2116
- //#endregion
2117
- //#region src/core/ScopedIterator.ts
2102
+ /**
2103
+ * Captures the exact rejection or synchronous throw from a driver conformance operation.
2104
+ *
2105
+ * @example
2106
+ * ```ts
2107
+ * await captureFailure(() => Promise.reject('refused')) // 'refused'
2108
+ * ```
2109
+ */
2110
+ async function captureFailure(operation) {
2111
+ try {
2112
+ await operation();
2113
+ return;
2114
+ } catch (error) {
2115
+ return error;
2116
+ }
2117
+ }
2118
+ // src/core/ScopedIterator.ts
2118
2119
  /**
2119
2120
  * Forms the internal continuation admission boundary for one scoped async iterable.
2120
2121
  *
@@ -2193,8 +2194,7 @@ var ScopedIterator = class {
2193
2194
  } catch {}
2194
2195
  }
2195
2196
  };
2196
- //#endregion
2197
- //#region src/core/TransactionScope.ts
2197
+ // src/core/TransactionScope.ts
2198
2198
  /**
2199
2199
  * Forms the internal lifetime boundary for one database transaction callback.
2200
2200
  *
@@ -2207,8 +2207,7 @@ var ScopedIterator = class {
2207
2207
  var TransactionScope = class {
2208
2208
  #operations = /* @__PURE__ */ new Set();
2209
2209
  #accepting = true;
2210
- #failed = false;
2211
- #error;
2210
+ #failure;
2212
2211
  get accepting() {
2213
2212
  return this.#accepting;
2214
2213
  }
@@ -2221,21 +2220,17 @@ var TransactionScope = class {
2221
2220
  } catch (error) {
2222
2221
  return Promise.reject(error);
2223
2222
  }
2224
- let promise;
2225
- try {
2226
- promise = operation();
2227
- } catch (error) {
2228
- promise = Promise.reject(error);
2229
- }
2223
+ const outcome = attempt(operation);
2224
+ const promise = outcome.success ? outcome.value : Promise.reject(outcome.error);
2230
2225
  this.#operations.add(promise);
2231
2226
  promise.then(() => {
2232
2227
  this.#operations.delete(promise);
2233
2228
  }, (error) => {
2234
2229
  this.#operations.delete(promise);
2235
- if (!this.#failed) {
2236
- this.#failed = true;
2237
- this.#error = error;
2238
- }
2230
+ this.#failure ??= {
2231
+ success: false,
2232
+ error
2233
+ };
2239
2234
  });
2240
2235
  return promise;
2241
2236
  }
@@ -2247,11 +2242,10 @@ var TransactionScope = class {
2247
2242
  }
2248
2243
  async drain() {
2249
2244
  while (this.#operations.size > 0) await Promise.allSettled(this.#operations);
2250
- if (this.#failed) throw this.#error;
2245
+ if (this.#failure !== void 0 && !this.#failure.success) throw this.#failure.error;
2251
2246
  }
2252
2247
  };
2253
- //#endregion
2254
- //#region src/core/DatabaseContext.ts
2248
+ // src/core/DatabaseContext.ts
2255
2249
  /**
2256
2250
  * Owns the internal shared state behind every typed view of one database.
2257
2251
  *
@@ -2268,7 +2262,7 @@ var DatabaseContext = class {
2268
2262
  #emitter;
2269
2263
  #operations = /* @__PURE__ */ new Set();
2270
2264
  #schema = [];
2271
- #transaction;
2265
+ #transactions = /* @__PURE__ */ new Set();
2272
2266
  #status = "idle";
2273
2267
  #ready;
2274
2268
  #failure;
@@ -2295,7 +2289,7 @@ var DatabaseContext = class {
2295
2289
  return this.#name;
2296
2290
  }
2297
2291
  get accepting() {
2298
- return this.#status !== "closed" && this.#transaction === void 0;
2292
+ return this.#status !== "closed" && this.#transactions.size === 0;
2299
2293
  }
2300
2294
  get status() {
2301
2295
  return this.#status;
@@ -2305,7 +2299,7 @@ var DatabaseContext = class {
2305
2299
  }
2306
2300
  register(schema) {
2307
2301
  if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
2308
- if (this.#ready !== void 0 || this.#transaction !== void 0) throw new DatabaseError("CONFLICT", `Database '${this.#name}' cannot import tables after opening has started`, {
2302
+ if (this.#ready !== void 0 || this.#transactions.size !== 0) throw new DatabaseError("CONFLICT", `Database '${this.#name}' cannot import tables after opening has started`, {
2309
2303
  name: this.#name,
2310
2304
  status: this.#status
2311
2305
  });
@@ -2359,12 +2353,8 @@ var DatabaseContext = class {
2359
2353
  } catch (error) {
2360
2354
  return Promise.reject(error);
2361
2355
  }
2362
- let promise;
2363
- try {
2364
- promise = operation();
2365
- } catch (error) {
2366
- promise = Promise.reject(error);
2367
- }
2356
+ const outcome = attempt(operation);
2357
+ const promise = outcome.success ? outcome.value : Promise.reject(outcome.error);
2368
2358
  this.#operations.add(promise);
2369
2359
  promise.then(() => {
2370
2360
  this.#operations.delete(promise);
@@ -2376,44 +2366,40 @@ var DatabaseContext = class {
2376
2366
  async transaction(scope, options) {
2377
2367
  checkAbort(options?.signal);
2378
2368
  this.#admit();
2379
- const token = {};
2380
- this.#transaction = token;
2369
+ const lifetime = new TransactionScope();
2370
+ this.#transactions.add(lifetime);
2381
2371
  try {
2382
2372
  await this.#drain();
2383
2373
  await this.connect();
2384
2374
  if (this.#driver.transaction !== void 0) {
2385
- const rejection = {
2386
- rejected: false,
2387
- error: void 0,
2388
- marker: {}
2389
- };
2375
+ const rejection = new AbortController();
2390
2376
  try {
2391
2377
  const value = await this.#driver.transaction(async (storage) => {
2392
2378
  this.#emitter.emit("transaction");
2393
- const outcome = await scope(storage, new TransactionScope());
2379
+ const outcome = await scope(storage, lifetime);
2394
2380
  if (!outcome.success) {
2395
- rejection.rejected = true;
2396
- rejection.error = outcome.error;
2397
- throw rejection.marker;
2381
+ rejection.abort(outcome);
2382
+ throw rejection.signal;
2398
2383
  }
2399
2384
  return outcome.value;
2400
2385
  });
2401
2386
  this.#emitter.emit("commit");
2402
2387
  return value;
2403
2388
  } catch (error) {
2404
- if (rejection.rejected) {
2405
- if (Object.is(error, rejection.marker)) {
2406
- this.#emitter.emit("rollback", rejection.error);
2407
- throw rejection.error;
2389
+ const failure = rejection.signal.reason;
2390
+ if (rejection.signal.aborted && isRecord(failure) && failure.success === false) {
2391
+ if (Object.is(error, rejection.signal)) {
2392
+ this.#emitter.emit("rollback", failure.error);
2393
+ throw failure.error;
2408
2394
  }
2409
- throw this.#rollbackError(rejection.error, error);
2395
+ throw this.#rollbackError(failure.error, error);
2410
2396
  }
2411
2397
  throw error;
2412
2398
  }
2413
2399
  }
2414
2400
  const rollback = await this.#driver.snapshot();
2415
2401
  this.#emitter.emit("transaction");
2416
- const outcome = await scope(this.#driver, new TransactionScope());
2402
+ const outcome = await scope(this.#driver, lifetime);
2417
2403
  if (outcome.success) {
2418
2404
  this.#emitter.emit("commit");
2419
2405
  return outcome.value;
@@ -2426,7 +2412,7 @@ var DatabaseContext = class {
2426
2412
  this.#emitter.emit("rollback", outcome.error);
2427
2413
  throw outcome.error;
2428
2414
  } finally {
2429
- if (this.#transaction === token) this.#transaction = void 0;
2415
+ this.#transactions.delete(lifetime);
2430
2416
  }
2431
2417
  }
2432
2418
  async migrate(deployed, options) {
@@ -2450,11 +2436,11 @@ var DatabaseContext = class {
2450
2436
  }
2451
2437
  #admit() {
2452
2438
  if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
2453
- if (this.#transaction !== void 0) throw new DatabaseError("CONFLICT", `Database '${this.#name}' has an active transaction`, { name: this.#name });
2439
+ if (this.#transactions.size !== 0) throw new DatabaseError("CONFLICT", `Database '${this.#name}' has an active transaction`, { name: this.#name });
2454
2440
  if (this.#failure !== void 0) throw this.#failure.error;
2455
2441
  }
2456
2442
  #outside() {
2457
- if (this.#transaction !== void 0) throw new DatabaseError("CONFLICT", `Database '${this.#name}' has an active transaction`, { name: this.#name });
2443
+ if (this.#transactions.size !== 0) throw new DatabaseError("CONFLICT", `Database '${this.#name}' has an active transaction`, { name: this.#name });
2458
2444
  }
2459
2445
  async #drain() {
2460
2446
  if (this.#operations.size === 0) return;
@@ -2535,8 +2521,7 @@ var DatabaseContext = class {
2535
2521
  });
2536
2522
  }
2537
2523
  };
2538
- //#endregion
2539
- //#region src/core/Cursor.ts
2524
+ // src/core/Cursor.ts
2540
2525
  /**
2541
2526
  * Walks a table's rows forward for bulk in-place mutation.
2542
2527
  *
@@ -2556,12 +2541,14 @@ var Cursor = class {
2556
2541
  #index = -1;
2557
2542
  #value;
2558
2543
  #closed = false;
2559
- constructor(keys, read, update, remove, track) {
2544
+ #initial;
2545
+ constructor(keys, read, update, remove, track, initial = false) {
2560
2546
  this.#keys = keys;
2561
2547
  this.#read = read;
2562
2548
  this.#update = update;
2563
2549
  this.#remove = remove;
2564
2550
  this.#track = track;
2551
+ this.#initial = initial;
2565
2552
  }
2566
2553
  get value() {
2567
2554
  return this.#value;
@@ -2573,6 +2560,13 @@ var Cursor = class {
2573
2560
  return this.#closed || this.#index >= this.#keys.length;
2574
2561
  }
2575
2562
  next() {
2563
+ if (this.#initial) {
2564
+ const initial = this.#queue(() => this.#advance());
2565
+ initial.then(() => {
2566
+ this.#initial = false;
2567
+ }, () => {});
2568
+ return initial;
2569
+ }
2576
2570
  return this.#track(() => this.#queue(() => this.#advance()));
2577
2571
  }
2578
2572
  update(changes) {
@@ -2629,8 +2623,33 @@ var Cursor = class {
2629
2623
  return result;
2630
2624
  }
2631
2625
  };
2632
- //#endregion
2633
- //#region src/core/Query.ts
2626
+ // src/core/StreamPage.ts
2627
+ /**
2628
+ * Tracks the matching-row position of one streaming page.
2629
+ *
2630
+ * @example
2631
+ * ```ts
2632
+ * const page = new StreamPage(1)
2633
+ * page.next() // false
2634
+ * page.next() // true
2635
+ * page.count // 1
2636
+ * ```
2637
+ */
2638
+ var StreamPage = class {
2639
+ #offset;
2640
+ #position = 0;
2641
+ constructor(offset) {
2642
+ this.#offset = offset;
2643
+ }
2644
+ get count() {
2645
+ return Math.max(0, this.#position - this.#offset);
2646
+ }
2647
+ next() {
2648
+ this.#position += 1;
2649
+ return this.#position > this.#offset;
2650
+ }
2651
+ };
2652
+ // src/core/Query.ts
2634
2653
  /**
2635
2654
  * Builds a read against one table through a fluent chain.
2636
2655
  *
@@ -2707,23 +2726,11 @@ var Query = class {
2707
2726
  }, options);
2708
2727
  return;
2709
2728
  }
2710
- const offset = this.#offset ?? 0;
2711
- let matched = 0;
2712
- let yielded = 0;
2729
+ const page = new StreamPage(this.#offset ?? 0);
2713
2730
  for await (const row of this.#table.scan({ conditions: this.#conditions }, options)) {
2714
- if (this.#limit !== void 0 && yielded >= this.#limit) break;
2715
- let matches = true;
2716
- for (const predicate of this.#filters) if (!predicate(row)) {
2717
- matches = false;
2718
- break;
2719
- }
2720
- if (!matches) continue;
2721
- if (matched < offset) {
2722
- matched += 1;
2723
- continue;
2724
- }
2725
- matched += 1;
2726
- yielded += 1;
2731
+ if (this.#limit !== void 0 && page.count >= this.#limit) break;
2732
+ if (!Iterator.from(this.#filters).every((predicate) => predicate(row))) continue;
2733
+ if (!page.next()) continue;
2727
2734
  yield row;
2728
2735
  }
2729
2736
  }
@@ -2732,9 +2739,7 @@ var Query = class {
2732
2739
  return this.#table.records({ conditions: this.#conditions }).then((fetched) => computeAggregate(this.#filtered(fetched), operation, column));
2733
2740
  }
2734
2741
  #filtered(rows) {
2735
- let result = rows;
2736
- for (const predicate of this.#filters) result = result.filter(predicate);
2737
- return result;
2742
+ return Iterator.from(this.#filters).reduce((result, predicate) => result.filter(predicate), rows);
2738
2743
  }
2739
2744
  #page(rows) {
2740
2745
  const offset = this.#offset ?? 0;
@@ -2742,8 +2747,7 @@ var Query = class {
2742
2747
  return rows.slice(offset, this.#limit === void 0 ? void 0 : offset + this.#limit);
2743
2748
  }
2744
2749
  };
2745
- //#endregion
2746
- //#region src/core/Table.ts
2750
+ // src/core/Table.ts
2747
2751
  /**
2748
2752
  * Exposes typed keyed CRUD plus fluent query and cursor access over a driver.
2749
2753
  *
@@ -2864,9 +2868,7 @@ var Table = class {
2864
2868
  const conditions = input?.conditions;
2865
2869
  const candidate = conditions === void 0 ? {} : { conditions };
2866
2870
  const rows = await this.#driver.records?.(this.#name, candidate) ?? filterRows(await this.#collect(), conditions ?? []);
2867
- let count = 0;
2868
- for (const row of rows) if (this.#guard(row)) count += 1;
2869
- return count;
2871
+ return Iterator.from(rows).reduce((count, row) => count + (this.#guard(row) ? 1 : 0), 0);
2870
2872
  });
2871
2873
  }
2872
2874
  /**
@@ -2958,10 +2960,8 @@ var Table = class {
2958
2960
  cursor() {
2959
2961
  return this.#track(async () => {
2960
2962
  await this.#ready();
2961
- let initializing = true;
2962
- const cursor = new Cursor(await this.#driver.keys(this.#name), (key) => this.#readCursor(key), (key, changes) => this.#updateCursor(key, changes), (key) => this.#deleteCursor(key), (operation) => initializing ? operation() : this.#track(operation));
2963
+ const cursor = new Cursor(await this.#driver.keys(this.#name), (key) => this.#readCursor(key), (key, changes) => this.#updateCursor(key, changes), (key) => this.#deleteCursor(key), (operation) => this.#track(operation), true);
2963
2964
  await cursor.next();
2964
- initializing = false;
2965
2965
  return cursor;
2966
2966
  });
2967
2967
  }
@@ -2971,14 +2971,13 @@ var Table = class {
2971
2971
  const conditions = input?.conditions;
2972
2972
  const offset = input?.offset ?? 0;
2973
2973
  const limit = input?.limit;
2974
- let matched = 0;
2975
- let yielded = 0;
2974
+ const page = new StreamPage(offset);
2976
2975
  const iterator = (this.#driver.stream === void 0 ? this.#driver.scan(this.#name) : this.#driver.stream(this.#name, conditions === void 0 ? {} : { conditions }))[Symbol.asyncIterator]();
2977
2976
  try {
2978
2977
  while (true) {
2979
2978
  await this.#ready();
2980
2979
  checkAbort(options?.signal);
2981
- if (limit !== void 0 && yielded >= limit) return;
2980
+ if (limit !== void 0 && page.count >= limit) return;
2982
2981
  const step = await iterator.next();
2983
2982
  await this.#ready();
2984
2983
  checkAbort(options?.signal);
@@ -2987,12 +2986,7 @@ var Table = class {
2987
2986
  if (conditions !== void 0 && conditions.length > 0 && !matchesQuery(row, conditions)) continue;
2988
2987
  const narrowed = this.#cast(row);
2989
2988
  if (narrowed === void 0) continue;
2990
- if (matched < offset) {
2991
- matched += 1;
2992
- continue;
2993
- }
2994
- matched += 1;
2995
- yielded += 1;
2989
+ if (!page.next()) continue;
2996
2990
  yield narrowed;
2997
2991
  }
2998
2992
  } finally {
@@ -3155,8 +3149,7 @@ var Table = class {
3155
3149
  return this.#scope === void 0 ? operation() : this.#scope.track(operation);
3156
3150
  }
3157
3151
  };
3158
- //#endregion
3159
- //#region src/core/DatabaseTransaction.ts
3152
+ // src/core/DatabaseTransaction.ts
3160
3153
  /**
3161
3154
  * Binds a table-only database view to one driver transaction scope.
3162
3155
  *
@@ -3192,8 +3185,7 @@ var DatabaseTransaction = class {
3192
3185
  return new Table(() => Promise.resolve(), this.#driver, name, key, contract, this.#generate, this.#error, void 0, this.#scope);
3193
3186
  }
3194
3187
  };
3195
- //#endregion
3196
- //#region src/core/Database.ts
3188
+ // src/core/Database.ts
3197
3189
  /**
3198
3190
  * Exposes a typed view over one shared internal lifecycle and storage context.
3199
3191
  *
@@ -3320,8 +3312,7 @@ var Database = class Database {
3320
3312
  return database;
3321
3313
  }
3322
3314
  };
3323
- //#endregion
3324
- //#region src/core/drivers/MemoryDriver.ts
3315
+ // src/core/drivers/MemoryDriver.ts
3325
3316
  /**
3326
3317
  * Implements the reference {@link DriverInterface} — nested maps, no I/O.
3327
3318
  *
@@ -3593,19 +3584,14 @@ var MemoryDriver = class {
3593
3584
  const conditions = input.conditions;
3594
3585
  const offset = input.offset ?? 0;
3595
3586
  const limit = input.limit;
3596
- let skipped = 0;
3597
- let yielded = 0;
3587
+ const page = new StreamPage(offset);
3598
3588
  for (const key of this.#ordered(table)) {
3599
- if (limit !== void 0 && yielded >= limit) return;
3589
+ if (limit !== void 0 && page.count >= limit) return;
3600
3590
  const row = store.get(key);
3601
3591
  if (row === void 0) continue;
3602
3592
  if (conditions !== void 0 && conditions.length > 0 && !matchesQuery(row, conditions)) continue;
3603
- if (skipped < offset) {
3604
- skipped += 1;
3605
- continue;
3606
- }
3593
+ if (!page.next()) continue;
3607
3594
  yield structuredClone(row);
3608
- yielded += 1;
3609
3595
  }
3610
3596
  }
3611
3597
  #store(table) {
@@ -3615,8 +3601,7 @@ var MemoryDriver = class {
3615
3601
  return store;
3616
3602
  }
3617
3603
  };
3618
- //#endregion
3619
- //#region src/core/factories.ts
3604
+ // src/core/factories.ts
3620
3605
  /**
3621
3606
  * Creates a database over a driver and a declared `tables` schema.
3622
3607
  *
@@ -3672,8 +3657,7 @@ function createDatabase(options) {
3672
3657
  function createMemoryDriver() {
3673
3658
  return new MemoryDriver();
3674
3659
  }
3675
- //#endregion
3676
- //#region src/core/DriverIterator.ts
3660
+ // src/core/DriverIterator.ts
3677
3661
  /**
3678
3662
  * Forms the internal continuation boundary for a root driver async iterator.
3679
3663
  *
@@ -3788,7 +3772,6 @@ var DriverIterator = class {
3788
3772
  } catch {}
3789
3773
  }
3790
3774
  };
3791
- //#endregion
3792
- export { CONFORMANCE_POSTS_SCHEMA, CONFORMANCE_SCHEMA, CONFORMANCE_USERS_SCHEMA, DEFAULT_PRIMARY, Database, DatabaseError, DriverIterator, MAX_PATTERN_LENGTH, MemoryDriver, applyQuery, auditDriver, bindRowKey, checkAbort, cloneDriverMetadata, cloneDriverSchema, cloneMigrationInput, compareValues, computeAggregate, conformDriver, createDatabase, createMemoryDriver, equalsValue, extractKey, filterRows, findColumn, isColumnSchema, isDatabaseError, isDriverMetadata, isDriverSchema, isKey, isMigration, isMigrationInput, isMigrationStep, isTableSchema, matchesCondition, matchesGlobPattern, matchesLikePattern, matchesQuery, matchesWildcardPattern, migrateRows, normalizeDriverSchema, planMigration, projectMigrationSchema, requireColumns, resolvePrimary, scanDriver, shapeToColumnSchema, shapeToColumnStorage, sortRows, validatePage };
3775
+ export { CONFORMANCE_POSTS_SCHEMA, CONFORMANCE_SCHEMA, CONFORMANCE_USERS_SCHEMA, DEFAULT_PRIMARY, Database, DatabaseError, DriverIterator, MAX_PATTERN_LENGTH, MemoryDriver, StreamPage, applyQuery, auditDriver, bindRowKey, captureFailure, checkAbort, cloneDriverMetadata, cloneDriverSchema, cloneMigrationInput, compareValues, computeAggregate, conformDriver, createDatabase, createMemoryDriver, equalsValue, extractKey, filterRows, findColumn, isColumnSchema, isDatabaseError, isDriverMetadata, isDriverSchema, isKey, isMigration, isMigrationInput, isMigrationStep, isTableSchema, matchesCondition, matchesGlobPattern, matchesLikePattern, matchesQuery, matchesWildcardPattern, matchesWildcardSegment, migrateRows, normalizeDriverSchema, planMigration, projectMigrationSchema, requireColumns, resolvePrimary, scanDriver, shapeToColumnSchema, shapeToColumnStorage, sortRows, validatePage };
3793
3776
 
3794
3777
  //# sourceMappingURL=index.js.map