@harperfast/rocksdb-js 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -80,6 +80,16 @@ function locateBinding() {
80
80
  throw new Error("Unable to locate rocksdb-js native binding");
81
81
  }
82
82
  const binding = req(locateBinding());
83
+ /**
84
+ * The native `BackgroundError` constructor (a real `Error` subclass). Exported
85
+ * as both a value — for `err instanceof BackgroundError` — and, via declaration
86
+ * merging with the interface above, a type. Instances are produced by the
87
+ * `'error'` event and `db.getLastError()`; consumers rarely construct their own.
88
+ * The `details` param is required and typed to the instance fields so a bare
89
+ * `new BackgroundError()` (which would omit `severity` / `writesDisabled` / …)
90
+ * is a type error.
91
+ */
92
+ const BackgroundError = binding.BackgroundError;
83
93
  const config = binding.config;
84
94
  const FRESH_VERSION_FLAG$1 = binding.constants.FRESH_VERSION_FLAG;
85
95
  const addGlobalListener = binding.addListener;
@@ -471,6 +481,150 @@ const backups = {
471
481
  }
472
482
  };
473
483
 
484
+ //#endregion
485
+ //#region src/count-estimator.ts
486
+ /**
487
+ * Below this many traversed entries the observed density is too noisy to
488
+ * calibrate with, so `estimate()` returns the raw statistical estimate.
489
+ */
490
+ const CALIBRATION_MIN_TRAVERSED = 16;
491
+ const CALIBRATION_MIN_CONFIDENCE = .8;
492
+ /**
493
+ * Bounds on how far the observed-vs-estimated ratio may scale the remainder;
494
+ * the statistical estimate can be arbitrarily wrong at data-block granularity
495
+ * and an unclamped ratio would let one bad sample dominate.
496
+ */
497
+ const CALIBRATION_MAX = 8;
498
+ /**
499
+ * Progressively refines a range key-count estimate as an iterator traverses
500
+ * the range. The estimate starts as the pure statistical estimate
501
+ * (`estimateCount`) and converges toward the exact count: the traversed
502
+ * portion is exact, and the statistical estimate of the untraversed remainder
503
+ * is calibrated by the observed ratio of actual-to-estimated entries over the
504
+ * portion already traversed.
505
+ *
506
+ * The estimator never touches the iterator itself — the caller reports
507
+ * progress with `advance(lastKey, count)` whenever it wants a checkpoint
508
+ * (e.g. once per page), then reads `estimate()`. The caller owns the
509
+ * progress contract: cursors must move monotonically through the range and
510
+ * each entry must be reported once (a re-reported page inflates the count
511
+ * undetectably). When traversal completes, call `finish()` so `estimate()`
512
+ * returns the exact total instead of adding a block-granular remainder.
513
+ *
514
+ * Each `estimate()` checkpoint queries RocksDB statistics for the two range
515
+ * segments (cost scales with the SSTs overlapping them, not with key count);
516
+ * results are memoized per checkpoint, so repeated reads between `advance()`
517
+ * calls are free.
518
+ */
519
+ var CountEstimator = class {
520
+ #store;
521
+ #start;
522
+ #end;
523
+ #exclusiveStart;
524
+ #inclusiveEnd;
525
+ #reverse;
526
+ #cursor;
527
+ #traversed = 0;
528
+ #finished = false;
529
+ #memoized;
530
+ constructor(store, options) {
531
+ this.#store = store;
532
+ this.#reverse = options?.reverse ?? false;
533
+ this.#start = this.#reverse ? options?.end : options?.start;
534
+ this.#end = this.#reverse ? options?.start : options?.end;
535
+ this.#exclusiveStart = options?.exclusiveStart ?? this.#reverse;
536
+ this.#inclusiveEnd = options?.inclusiveEnd ?? this.#reverse;
537
+ }
538
+ /**
539
+ * The number of entries reported traversed so far.
540
+ */
541
+ get traversed() {
542
+ return this.#traversed;
543
+ }
544
+ /**
545
+ * Records that iteration has advanced through `count` more entries, ending
546
+ * at `lastKey`. Pass `count: 0` when an empty page has no last key.
547
+ */
548
+ advance(lastKey, count = 1) {
549
+ if (lastKey === void 0) {
550
+ if (count !== 0) throw new Error("CountEstimator.advance requires lastKey when count is nonzero");
551
+ return;
552
+ }
553
+ this.#cursor = lastKey;
554
+ this.#traversed += count;
555
+ this.#memoized = void 0;
556
+ }
557
+ /**
558
+ * Marks traversal of the range as complete: `estimate()` becomes the exact
559
+ * traversed count.
560
+ */
561
+ finish() {
562
+ this.#finished = true;
563
+ }
564
+ /**
565
+ * Estimates the total number of entries in the full range: the exact
566
+ * traversed count plus a calibrated statistical estimate of the remainder.
567
+ * `confidence` is the exactness-weighted blend of the traversed portion
568
+ * (exact) and the remainder's statistical confidence, so it approaches 1
569
+ * as the exact portion grows (though a checkpoint can decrease when
570
+ * calibration makes a large correction), and is exactly 1 after `finish()`.
571
+ */
572
+ estimate() {
573
+ if (this.#finished) return {
574
+ count: this.#traversed,
575
+ confidence: 1
576
+ };
577
+ if (this.#memoized !== void 0) return { ...this.#memoized };
578
+ if (this.#cursor === void 0) {
579
+ this.#memoized = this.#store.estimateCount({
580
+ start: this.#start,
581
+ end: this.#end,
582
+ exclusiveStart: this.#exclusiveStart,
583
+ inclusiveEnd: this.#inclusiveEnd
584
+ });
585
+ return { ...this.#memoized };
586
+ }
587
+ const traversedRange = this.#reverse ? {
588
+ start: this.#cursor,
589
+ end: this.#end,
590
+ inclusiveEnd: this.#inclusiveEnd
591
+ } : {
592
+ start: this.#start,
593
+ exclusiveStart: this.#exclusiveStart,
594
+ end: this.#cursor,
595
+ inclusiveEnd: true
596
+ };
597
+ const remainingRange = this.#reverse ? {
598
+ start: this.#start,
599
+ exclusiveStart: this.#exclusiveStart,
600
+ end: this.#cursor
601
+ } : {
602
+ start: this.#cursor,
603
+ exclusiveStart: true,
604
+ end: this.#end,
605
+ inclusiveEnd: this.#inclusiveEnd
606
+ };
607
+ const remainingEstimate = this.#store.estimateCount(remainingRange);
608
+ let remaining = remainingEstimate.count;
609
+ let calibrationConfidence = 1;
610
+ if (this.#traversed >= CALIBRATION_MIN_TRAVERSED) {
611
+ const traversedEstimate = this.#store.estimateCount(traversedRange);
612
+ if (traversedEstimate.count >= CALIBRATION_MIN_TRAVERSED && traversedEstimate.confidence >= CALIBRATION_MIN_CONFIDENCE) {
613
+ const calibration = Math.min(CALIBRATION_MAX, Math.max(1 / CALIBRATION_MAX, this.#traversed / traversedEstimate.count));
614
+ remaining *= calibration;
615
+ calibrationConfidence = Math.min(calibration, 1 / calibration);
616
+ }
617
+ }
618
+ const count = Math.round(this.#traversed + remaining);
619
+ const confidence = count > 0 ? Math.min(.999, (this.#traversed + calibrationConfidence * remainingEstimate.confidence * remaining) / count) : Math.min(.999, calibrationConfidence * remainingEstimate.confidence);
620
+ this.#memoized = {
621
+ count,
622
+ confidence
623
+ };
624
+ return { ...this.#memoized };
625
+ }
626
+ };
627
+
474
628
  //#endregion
475
629
  //#region src/util.ts
476
630
  /**
@@ -1336,6 +1490,11 @@ var Store = class {
1336
1490
  */
1337
1491
  freezeData;
1338
1492
  /**
1493
+ * Verbosity of RocksDB's informational logging (`info_log_level`). Omit to
1494
+ * leave RocksDB's own default.
1495
+ */
1496
+ infoLogLevel;
1497
+ /**
1339
1498
  * Reusable buffer for encoding keys.
1340
1499
  */
1341
1500
  keyBuffer;
@@ -1348,6 +1507,16 @@ var Store = class {
1348
1507
  */
1349
1508
  maxKeySize;
1350
1509
  /**
1510
+ * Per-file size cap (bytes) for informational log files (`LOG` /
1511
+ * `LOG.old.*`). Bounds total informational-log footprint to roughly `5 *
1512
+ * maxLogFileSize` (RocksDB retains up to 5 of these files). A value of `0`
1513
+ * is RocksDB's "single unbounded log file" mode: it disables size-based
1514
+ * rotation, so the log can grow without limit — set it only if you want that.
1515
+ *
1516
+ * @default 16777216 (16MB)
1517
+ */
1518
+ maxLogFileSize;
1519
+ /**
1351
1520
  * The maximum number of table files RocksDB keeps open. `0` (the default)
1352
1521
  * derives a budget from the effective per-process open-file limit (an
1353
1522
  * eighth of the limit — several databases can share one process — clamped
@@ -1366,13 +1535,26 @@ var Store = class {
1366
1535
  /**
1367
1536
  * The bytes of recent memtable history to retain in memory for transaction
1368
1537
  * conflict checking. `-1` derives the value from
1369
- * `maxWriteBufferNumber * writeBufferSize`.
1538
+ * `maxWriteBufferNumber * writeBufferSize`, except under a stalling
1539
+ * `writeBufferManager`, where it derives `0`: retained history is charged to
1540
+ * that manager and is only trimmable down to this target, so a target the
1541
+ * budget cannot hold stalls writes permanently. Set a number to size it
1542
+ * yourself against the budget and the column-family count.
1370
1543
  */
1371
1544
  maxWriteBufferSizeToMaintain;
1372
1545
  /**
1373
1546
  * The total memtable budget in bytes across all column families. When the
1374
- * sum of memtables reaches this size, RocksDB flushes the largest one. `0`
1375
- * disables the global trigger so per-CF `writeBufferSize` drives flushing.
1547
+ * sum of memtables reaches this size, RocksDB flushes the largest one.
1548
+ * Defaults to `0`, which disables the global trigger so per-CF
1549
+ * `writeBufferSize` drives flushing.
1550
+ *
1551
+ * Set this only if you know your column-family count: it is one budget split
1552
+ * across all of them, so a value that suits a few families starves many. To
1553
+ * bound total memtable memory instead, configure a WriteBufferManager.
1554
+ *
1555
+ * Database-wide, so it binds when the path is first opened in this process:
1556
+ * a later open of the same path — including from another worker thread —
1557
+ * keeps the first opener's value rather than overriding or rejecting it.
1376
1558
  */
1377
1559
  dbWriteBufferSize;
1378
1560
  /**
@@ -1401,7 +1583,9 @@ var Store = class {
1401
1583
  pessimistic;
1402
1584
  /**
1403
1585
  * Whether the database is open in readonly mode. When `true`, write
1404
- * operations will throw an error with code `ERR_DATABASE_READONLY`.
1586
+ * operations will throw an error with code `ERR_DATABASE_READONLY`, except
1587
+ * for `flush()`/`flushSync()`/`compact()`/`compactSync()`, which succeed as
1588
+ * no-ops.
1405
1589
  */
1406
1590
  readOnly;
1407
1591
  /**
@@ -1428,8 +1612,8 @@ var Store = class {
1428
1612
  */
1429
1613
  transactionLogMaxAgeThreshold;
1430
1614
  /**
1431
- * The maximum size of a transaction log before it is rotated to the next
1432
- * sequence number.
1615
+ * The target maximum size of a transaction log before rotation. Transactions
1616
+ * are never split across files, so one transaction may exceed this target.
1433
1617
  */
1434
1618
  transactionLogMaxSize;
1435
1619
  /**
@@ -1477,9 +1661,11 @@ var Store = class {
1477
1661
  this.encoder = options?.encoder ?? null;
1478
1662
  this.encoding = options?.encoding ?? null;
1479
1663
  this.freezeData = options?.freezeData ?? false;
1664
+ this.infoLogLevel = options?.infoLogLevel;
1480
1665
  this.keyBuffer = KEY_BUFFER;
1481
1666
  this.keyEncoding = keyEncoding;
1482
1667
  this.maxKeySize = options?.maxKeySize ?? MAX_KEY_SIZE;
1668
+ this.maxLogFileSize = options?.maxLogFileSize;
1483
1669
  this.maxOpenFiles = options?.maxOpenFiles;
1484
1670
  this.maxWriteBufferNumber = options?.maxWriteBufferNumber;
1485
1671
  this.maxWriteBufferSizeToMaintain = options?.maxWriteBufferSizeToMaintain;
@@ -1636,6 +1822,24 @@ var Store = class {
1636
1822
  }
1637
1823
  return result;
1638
1824
  }
1825
+ /**
1826
+ * Estimates the number of keys in a range from RocksDB statistics
1827
+ * (memtable stats + approximate SST sizes with range-local entry density)
1828
+ * without iterating. Estimates always reflect committed state, so there is
1829
+ * no transactional variant.
1830
+ */
1831
+ estimateCount(options) {
1832
+ let startBuffer;
1833
+ let endBuffer;
1834
+ const reverse = options?.reverse ?? false;
1835
+ const start = reverse ? options?.end : options?.start;
1836
+ const end = reverse ? options?.start : options?.end;
1837
+ const exclusiveStart = options?.exclusiveStart ?? reverse;
1838
+ const inclusiveEnd = options?.inclusiveEnd ?? reverse;
1839
+ if (start !== void 0) startBuffer = copyEncodedKey(this.encodeKey(start), exclusiveStart);
1840
+ if (end !== void 0) endBuffer = copyEncodedKey(this.encodeKey(end), inclusiveEnd);
1841
+ return this.db.estimateCount(startBuffer, endBuffer);
1842
+ }
1639
1843
  getCount(context, options) {
1640
1844
  options = { ...options };
1641
1845
  if (options?.start !== void 0) {
@@ -1787,6 +1991,8 @@ var Store = class {
1787
1991
  dbWriteBufferSize: this.dbWriteBufferSize,
1788
1992
  disableWAL: this.disableWAL,
1789
1993
  enableStats: this.enableStats,
1994
+ infoLogLevel: this.infoLogLevel,
1995
+ maxLogFileSize: this.maxLogFileSize,
1790
1996
  maxOpenFiles: this.maxOpenFiles,
1791
1997
  maxWriteBufferNumber: this.maxWriteBufferNumber,
1792
1998
  maxWriteBufferSizeToMaintain: this.maxWriteBufferSizeToMaintain,
@@ -1847,7 +2053,7 @@ var Store = class {
1847
2053
  useLog(context, name) {
1848
2054
  if (typeof name !== "string" && typeof name !== "number") throw new TypeError("Log name must be a string or number");
1849
2055
  if (typeof name === "string" && /[\t\n\r\\/]/.test(name)) throw new Error(`Invalid transaction log name "${name}"`);
1850
- return context.useLog(String(name));
2056
+ return context.useLog(String(name), this.db);
1851
2057
  }
1852
2058
  /**
1853
2059
  * Checks the process-global verification table for a fresh version match
@@ -1881,6 +2087,18 @@ var Store = class {
1881
2087
  }
1882
2088
  };
1883
2089
  /**
2090
+ * Copies an encoded key out of the shared key buffer (which the next
2091
+ * `encodeKey` call would clobber), optionally appending a zero byte to
2092
+ * produce the key's bytewise successor.
2093
+ */
2094
+ function copyEncodedKey(encoded, appendSuccessorByte) {
2095
+ const length = encoded.end - encoded.start;
2096
+ const copy = Buffer.allocUnsafe(length + (appendSuccessorByte ? 1 : 0));
2097
+ copy.set(encoded.subarray(encoded.start, encoded.end));
2098
+ if (appendSuccessorByte) copy[length] = 0;
2099
+ return copy;
2100
+ }
2101
+ /**
1884
2102
  * Ensure that they key has been copied into our shared buffer, and return the ending position
1885
2103
  * @param keyBuffer
1886
2104
  */
@@ -2077,6 +2295,16 @@ var Transaction = class extends DBI {
2077
2295
 
2078
2296
  //#endregion
2079
2297
  //#region src/database.ts
2298
+ const BACKGROUND_ERROR_SEVERITY_NAMES = [
2299
+ "none",
2300
+ "soft",
2301
+ "hard",
2302
+ "fatal",
2303
+ "unrecoverable"
2304
+ ];
2305
+ function backgroundErrorSeverityName(severity) {
2306
+ return BACKGROUND_ERROR_SEVERITY_NAMES[severity] ?? "unknown";
2307
+ }
2080
2308
  /**
2081
2309
  * The main class for interacting with a RocksDB database.
2082
2310
  *
@@ -2148,6 +2376,9 @@ var RocksDatabase = class RocksDatabase extends DBI {
2148
2376
  * Compacts the entire key range of the database asynchronously.
2149
2377
  * This triggers manual compaction which removes tombstones and reclaims space.
2150
2378
  *
2379
+ * On a read-only database this is a no-op: arguments are still validated, but
2380
+ * the returned promise resolves without compacting rather than rejecting.
2381
+ *
2151
2382
  * @example
2152
2383
  * ```typescript
2153
2384
  * const db = RocksDatabase.open('/path/to/database');
@@ -2199,6 +2430,9 @@ var RocksDatabase = class RocksDatabase extends DBI {
2199
2430
  * Compacts the entire key range of the database synchronously.
2200
2431
  * This triggers manual compaction which removes tombstones and reclaims space.
2201
2432
  *
2433
+ * On a read-only database this validates its arguments and then returns
2434
+ * without compacting, rather than throwing.
2435
+ *
2202
2436
  * @example
2203
2437
  * ```typescript
2204
2438
  * const db = RocksDatabase.open('/path/to/database');
@@ -2231,6 +2465,24 @@ var RocksDatabase = class RocksDatabase extends DBI {
2231
2465
  return this.store.db.getCompression();
2232
2466
  }
2233
2467
  /**
2468
+ * The informational-log settings currently in effect for this database,
2469
+ * read live from RocksDB, as `{ maxLogFileSize, infoLogLevel }`. These are
2470
+ * database-wide settings (not per-column-family): `maxLogFileSize` bounds
2471
+ * each retained `LOG` / `LOG.old.*` file's size (defaults to 16MB, so with
2472
+ * RocksDB's `keep_log_file_num = 5` the total footprint is bounded at
2473
+ * roughly 80MB); `infoLogLevel` is the logging verbosity. The database must
2474
+ * be open.
2475
+ *
2476
+ * @example
2477
+ * ```typescript
2478
+ * const db = RocksDatabase.open('/path/to/db', { maxLogFileSize: 4 * 1024 * 1024 });
2479
+ * db.logOptions; // { maxLogFileSize: 4194304, infoLogLevel: 1 }
2480
+ * ```
2481
+ */
2482
+ get logOptions() {
2483
+ return this.store.db.getLogOptions();
2484
+ }
2485
+ /**
2234
2486
  * Set global database settings.
2235
2487
  *
2236
2488
  * @param options - The options for the database.
@@ -2314,18 +2566,107 @@ var RocksDatabase = class RocksDatabase extends DBI {
2314
2566
  /**
2315
2567
  * Flushes the underlying database by performing a commit or clearing any buffered operations.
2316
2568
  *
2569
+ * On a read-only database this is a no-op: the options bag is still validated, but the
2570
+ * returned promise resolves without flushing rather than rejecting.
2571
+ *
2572
+ * @param options - Flush options; see {@link FlushOptions.allowWriteStall} before relying on
2573
+ * this resolving promptly under write pressure.
2317
2574
  * @return {void} Does not return a value.
2318
2575
  */
2319
- flush() {
2320
- return new Promise((resolve, reject) => this.store.db.flush(resolve, reject));
2576
+ flush(options) {
2577
+ return new Promise((resolve, reject) => this.store.db.flush(resolve, reject, options));
2321
2578
  }
2322
2579
  /**
2323
2580
  * Synchronously flushes the underlying database by performing a commit or clearing any buffered operations.
2324
2581
  *
2582
+ * The default `allowWriteStall: false` wait is taken on the JS thread here, so it freezes the
2583
+ * event loop rather than parking a libuv worker — a larger blast radius than {@link flush}, not
2584
+ * a smaller one.
2585
+ *
2586
+ * On a read-only database this validates its options and then returns without flushing, rather
2587
+ * than throwing.
2588
+ *
2589
+ * @param options - Flush options; see {@link FlushOptions.allowWriteStall}.
2325
2590
  * @return {void} Does not return a value.
2326
2591
  */
2327
- flushSync() {
2328
- return this.store.db.flushSync();
2592
+ flushSync(options) {
2593
+ return this.store.db.flushSync(options);
2594
+ }
2595
+ /**
2596
+ * Returns the most recent {@link BackgroundError} observed on this database, or
2597
+ * `null` when none has occurred. The `'error'` event is the push counterpart;
2598
+ * this is the pull equivalent for on-demand checks (e.g. a health probe) and
2599
+ * for catching an error that fired after open but before a listener was
2600
+ * attached. (A background error that occurs *during* the initial open is a
2601
+ * narrow startup window not yet captured, addressed separately.)
2602
+ *
2603
+ * Purely historical — it is not cleared by a successful {@link RocksDatabase.resume}.
2604
+ * When the returned error's `writesDisabled` is `true`, writes are stopped
2605
+ * until recovery.
2606
+ *
2607
+ * @example
2608
+ * ```typescript
2609
+ * const err = db.getLastError();
2610
+ * if (err?.writesDisabled) {
2611
+ * // ...free disk space...
2612
+ * db.resume();
2613
+ * }
2614
+ * ```
2615
+ */
2616
+ getLastError() {
2617
+ return this.store.db.getLastError();
2618
+ }
2619
+ /**
2620
+ * Sets or clears the last background error, mirroring the Win32
2621
+ * `SetLastError`/`GetLastError` pair. Pass `null` (or no argument) to **reset**
2622
+ * it — {@link RocksDatabase.getLastError} then returns `null` and no event
2623
+ * fires; this is the intended way to clear the error after handling or
2624
+ * recovering it (e.g. after {@link RocksDatabase.resume}). Pass an object to
2625
+ * set it: it is stored and the `'error'` event fires with the reconstructed
2626
+ * {@link BackgroundError}. `type` defaults to `'background'`.
2627
+ *
2628
+ * @example
2629
+ * ```typescript
2630
+ * db.resume();
2631
+ * db.setLastError(null); // recovered — clear the historical error
2632
+ * ```
2633
+ */
2634
+ setLastError(error) {
2635
+ if (error == null) return this.store.db.setLastError(null);
2636
+ const severity = error.severity ?? 0;
2637
+ return this.store.db.setLastError({
2638
+ ...error,
2639
+ type: error.type ?? "background",
2640
+ severity,
2641
+ severityName: error.severityName ?? backgroundErrorSeverityName(severity),
2642
+ writesDisabled: error.writesDisabled ?? severity >= 2
2643
+ });
2644
+ }
2645
+ /**
2646
+ * Attempts to recover the database from a background error by calling
2647
+ * RocksDB's `DB::Resume()`. When a write fails at the filesystem level (e.g. a
2648
+ * full disk), RocksDB stops accepting writes and this database emits an
2649
+ * {@link BackgroundError} via the `'error'` event. Call `resume()` after the
2650
+ * underlying condition has cleared — e.g. once disk space has been freed. On
2651
+ * success writes are accepted again; on failure (the condition has not
2652
+ * actually cleared) it throws and the database stays read-only. A no-op on a
2653
+ * healthy database.
2654
+ *
2655
+ * Runs synchronously and may briefly block, since recovery can re-flush
2656
+ * memtables.
2657
+ *
2658
+ * @example
2659
+ * ```typescript
2660
+ * db.on('error', (err) => {
2661
+ * if (err.writesDisabled) {
2662
+ * // ...free disk space...
2663
+ * db.resume();
2664
+ * }
2665
+ * });
2666
+ * ```
2667
+ */
2668
+ resume() {
2669
+ return this.store.db.resume();
2329
2670
  }
2330
2671
  /**
2331
2672
  * Gets a RocksDB database property as an integer.
@@ -2360,7 +2701,9 @@ var RocksDatabase = class RocksDatabase extends DBI {
2360
2701
  return this.store.db.getDBProperty(propertyName);
2361
2702
  }
2362
2703
  /**
2363
- * Retrieves the estimated number of keys in the database.
2704
+ * Retrieves the estimated number of keys in the database. This is an alias
2705
+ * for `db.estimateCount().count`; use `estimateCount()` for range support
2706
+ * and a confidence indicator.
2364
2707
  *
2365
2708
  * @example
2366
2709
  * ```typescript
@@ -2373,6 +2716,58 @@ var RocksDatabase = class RocksDatabase extends DBI {
2373
2716
  return this.getDBIntProperty("rocksdb.estimate-num-keys") ?? 0;
2374
2717
  }
2375
2718
  /**
2719
+ * Estimates the number of keys in the database, or within a key range,
2720
+ * returning `{ count, confidence }`. Unlike `getKeysCount()`, this never
2721
+ * iterates: the estimate is derived from RocksDB statistics (memtable
2722
+ * stats plus approximate SST sizes converted through the entry density of
2723
+ * the SSTs overlapping the range), so its cost scales with the number of
2724
+ * SSTs overlapping the range rather than the number of keys — though
2725
+ * reading table properties for cold files can do I/O through the table
2726
+ * cache, and a start-only range does the work of its complement below
2727
+ * `start`. Accuracy improves with range size. Resolution is bounded by SST
2728
+ * data-block granularity, so tiny ranges may over-report or report zero for
2729
+ * present keys; low `confidence` is the signal. Recently deleted or
2730
+ * overwritten entries may be counted until compaction. An inverted range
2731
+ * (`start` >= `end`) returns `{ count: 0, confidence: 1 }`.
2732
+ *
2733
+ * `confidence` is a heuristic 0–1 trust indicator (1 only when exact) —
2734
+ * see `CountEstimate`. Estimates always reflect committed state; writes
2735
+ * pending in a transaction are not included.
2736
+ *
2737
+ * @example
2738
+ * ```typescript
2739
+ * const db = RocksDatabase.open('/path/to/database');
2740
+ * const { count, confidence } = db.estimateCount({ start: 'a', end: 'z' });
2741
+ * ```
2742
+ */
2743
+ estimateCount(options) {
2744
+ return this.store.estimateCount(options);
2745
+ }
2746
+ /**
2747
+ * Creates a `CountEstimator` for progressively refining a range count
2748
+ * estimate while iterating the range: report progress with
2749
+ * `advance(lastKey, count)` (e.g. once per page of results) and
2750
+ * `estimate()` returns the exact traversed count plus a calibrated
2751
+ * estimate of the remainder, converging toward the exact total. Call
2752
+ * `finish()` when traversal completes to make `estimate()` exact.
2753
+ *
2754
+ * @example
2755
+ * ```typescript
2756
+ * const estimator = db.createCountEstimator({ start: 'a', end: 'z' });
2757
+ * let lastKey;
2758
+ * let pageSize = 0;
2759
+ * for (const { key } of db.getRange({ start: 'a', end: 'z', limit: 25 })) {
2760
+ * lastKey = key;
2761
+ * pageSize++;
2762
+ * }
2763
+ * estimator.advance(lastKey, pageSize);
2764
+ * const { count, confidence } = estimator.estimate();
2765
+ * ```
2766
+ */
2767
+ createCountEstimator(options) {
2768
+ return new CountEstimator(this.store, options);
2769
+ }
2770
+ /**
2376
2771
  * Returns the current timestamp as a monotonically increasing timestamp in
2377
2772
  * milliseconds represented as a decimal number.
2378
2773
  *
@@ -2878,6 +3273,98 @@ const FLOAT_TO_UINT32 = /* @__PURE__ */ new Float64Array(1);
2878
3273
  const UINT32_FROM_FLOAT = new Uint32Array(FLOAT_TO_UINT32.buffer);
2879
3274
  const { TRANSACTION_LOG_FILE_HEADER_SIZE, TRANSACTION_LOG_ENTRY_HEADER_SIZE } = constants;
2880
3275
  /**
3276
+ * Number of consecutive well-formed frames that, on their own, signal that real log data has
3277
+ * resumed after a framing break. Kept in step with `RESYNC_MIN_FRAMES` in
3278
+ * `src/binding/transaction_log/transaction_log_recovery.cpp`, which uses the same heuristic to
3279
+ * classify a break as mid-file corruption (valid entries follow) rather than a torn tail.
3280
+ */
3281
+ const RESYNC_MIN_FRAMES = 8;
3282
+ /**
3283
+ * Thrown when an entry's framing is broken. `resyncPosition` is the offset where valid framing
3284
+ * resumes, when one was found: the frames after a mid-log break are intact and reachable, and
3285
+ * only the broken span between `position` and `resyncPosition` is unreadable. It is `undefined`
3286
+ * for a torn tail, where nothing follows the break.
3287
+ *
3288
+ * The iterator is left positioned at `resyncPosition` (or at end-of-log when there is none), so a
3289
+ * caller that chooses to recover the entries past the break calls `next()` again.
3290
+ */
3291
+ var CorruptFrameError = class extends RangeError {
3292
+ /** Sequence number of the log file the break is in. */
3293
+ logId;
3294
+ /** Offset of the broken frame. */
3295
+ position;
3296
+ /** Offset where valid framing resumes, or `undefined` when nothing valid follows. */
3297
+ resyncPosition;
3298
+ /** Bytes between the break and the resume point that cannot be read. */
3299
+ unreadableBytes;
3300
+ constructor(message, logId, position, resyncPosition) {
3301
+ super(resyncPosition === void 0 ? message : `${message}; valid framing resumes at ${resyncPosition.toString(16)}, ${resyncPosition - position} byte(s) unreadable`);
3302
+ this.name = "CorruptFrameError";
3303
+ this.logId = logId;
3304
+ this.position = position;
3305
+ this.resyncPosition = resyncPosition;
3306
+ this.unreadableBytes = resyncPosition === void 0 ? 0 : resyncPosition - position;
3307
+ }
3308
+ };
3309
+ /**
3310
+ * Whether a complete, in-bounds frame begins at `pos`. The only sane bound on an entry's length is
3311
+ * the written extent: a single entry can legitimately exceed the rotation threshold (the first
3312
+ * entry written to a fresh file is always written in full). A zero timestamp is the
3313
+ * end-of-entries marker, not a frame.
3314
+ */
3315
+ function frameFits(dataView, pos, dataEnd) {
3316
+ if (pos + TRANSACTION_LOG_ENTRY_HEADER_SIZE > dataEnd) return false;
3317
+ if (dataView.getFloat64(pos) === 0) return false;
3318
+ const length = dataView.getUint32(pos + 8);
3319
+ if (length === 0) return false;
3320
+ return pos + TRANSACTION_LOG_ENTRY_HEADER_SIZE + length <= dataEnd;
3321
+ }
3322
+ /**
3323
+ * Finds the offset in `[from, dataEnd)` where valid log framing resumes: a run of at least
3324
+ * `RESYNC_MIN_FRAMES` well-formed frames, or a run that lands exactly on `dataEnd`. Returns
3325
+ * `undefined` when nothing resumes (a torn tail).
3326
+ *
3327
+ * `dataEnd` must be the **written extent**, not the mapped capacity. The log's memory map is
3328
+ * pre-extended well past the data, and every offset inside that zero padding reads as an
3329
+ * end-of-entries marker — so scanning against the map would both lose the exact-end signal (the
3330
+ * chain can never land on a capacity that data does not reach) and, if a zero were accepted as a
3331
+ * terminator, let a chain "end" anywhere in megabytes of padding. Against the written extent the
3332
+ * end is a single offset, so a chain landing on it is ~1/2^32 to be coincidence.
3333
+ *
3334
+ * The JS counterpart of `validFramingResumes()` in `transaction_log_recovery.cpp`, which answers
3335
+ * the same question at open time but keeps only the yes/no, discarding the offset.
3336
+ */
3337
+ function findResyncPosition(dataView, from, dataEnd) {
3338
+ const lastStart = dataEnd - TRANSACTION_LOG_ENTRY_HEADER_SIZE;
3339
+ for (let start = from; start <= lastStart; start++) {
3340
+ let pos = start;
3341
+ let frames = 0;
3342
+ while (frames < RESYNC_MIN_FRAMES && frameFits(dataView, pos, dataEnd)) {
3343
+ pos += TRANSACTION_LOG_ENTRY_HEADER_SIZE + dataView.getUint32(pos + 8);
3344
+ if (++frames >= RESYNC_MIN_FRAMES || pos === dataEnd) return start;
3345
+ }
3346
+ }
3347
+ }
3348
+ /**
3349
+ * Builds the report for a broken frame, with the offset iteration should continue from. Resolves
3350
+ * the written extent here rather than per frame: `getLogFileSize` crosses into native and takes
3351
+ * the store mutex, which a healthy read must never pay.
3352
+ */
3353
+ function corruptFrame(message, transactionLog, dataView, logBuffer, position, limit, size, readUncommitted) {
3354
+ let dataEnd = limit;
3355
+ if (readUncommitted) try {
3356
+ const writtenExtent = transactionLog.getLogFileSize(logBuffer.logId);
3357
+ dataEnd = writtenExtent > 0 ? Math.min(logBuffer.length, writtenExtent) : 0;
3358
+ } catch {
3359
+ dataEnd = 0;
3360
+ }
3361
+ const resyncPosition = findResyncPosition(dataView, position + 1, dataEnd);
3362
+ return {
3363
+ error: new CorruptFrameError(message, logBuffer.logId, position, resyncPosition),
3364
+ nextPosition: resyncPosition ?? Math.max(dataEnd || limit, size, position + 1)
3365
+ };
3366
+ }
3367
+ /**
2881
3368
  * Returns an iterable for transaction entries within the specified range of timestamps
2882
3369
  * This iterable can be iterated over multiple times, and subsequent iterations will continue
2883
3370
  * from where the last iteration left off, allowing for iteration through the log file
@@ -2967,9 +3454,17 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
2967
3454
  value: void 0
2968
3455
  };
2969
3456
  const limit = readUncommitted ? logBuffer.length : Math.min(size, logBuffer.length);
2970
- if (position + TRANSACTION_LOG_ENTRY_HEADER_SIZE > limit) throw new RangeError(`Corrupt transaction log: truncated entry header at position ${position.toString(16)} of log ${logBuffer.logId} (available=${limit - position})`);
3457
+ if (position + TRANSACTION_LOG_ENTRY_HEADER_SIZE > limit) {
3458
+ const broken = corruptFrame(`Corrupt transaction log: truncated entry header at position ${position.toString(16)} of log ${logBuffer.logId} (available=${limit - position})`, transactionLog, dataView, logBuffer, position, limit, size, !!readUncommitted);
3459
+ position = broken.nextPosition;
3460
+ throw broken.error;
3461
+ }
2971
3462
  const length = dataView.getUint32(position + 8);
2972
- if (position + TRANSACTION_LOG_ENTRY_HEADER_SIZE + length > limit) throw new RangeError(`Corrupt transaction log entry at position ${position.toString(16)} of log ${logBuffer.logId}: declared length ${length} overruns the log (limit=${limit})`);
3463
+ if (length === 0 || position + TRANSACTION_LOG_ENTRY_HEADER_SIZE + length > limit) {
3464
+ const broken = corruptFrame(length === 0 ? `Corrupt transaction log entry at position ${position.toString(16)} of log ${logBuffer.logId}: zero-length entry` : `Corrupt transaction log entry at position ${position.toString(16)} of log ${logBuffer.logId}: declared length ${length} overruns the log (limit=${limit})`, transactionLog, dataView, logBuffer, position, limit, size, !!readUncommitted);
3465
+ position = broken.nextPosition;
3466
+ throw broken.error;
3467
+ }
2973
3468
  position += TRANSACTION_LOG_ENTRY_HEADER_SIZE;
2974
3469
  let matchesRange;
2975
3470
  if (foundExactStart) matchesRange = (!exclusiveStart || timestamp !== start) && timestamp < end;
@@ -3060,10 +3555,13 @@ function loadLastPosition(transactionLog, readUncommitted) {
3060
3555
  //#region src/index.ts
3061
3556
  const versions = {
3062
3557
  rocksdb: version,
3063
- "rocksdb-js": "2.7.0"
3558
+ "rocksdb-js": "2.8.0"
3064
3559
  };
3065
3560
 
3066
3561
  //#endregion
3562
+ exports.BackgroundError = BackgroundError;
3563
+ exports.CorruptFrameError = CorruptFrameError;
3564
+ exports.CountEstimator = CountEstimator;
3067
3565
  exports.DBI = DBI;
3068
3566
  exports.DBIterator = DBIterator;
3069
3567
  exports.RocksDatabase = RocksDatabase;