@harperfast/rocksdb-js 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -74,6 +74,11 @@ Creates a new database instance.
74
74
  before purging. Defaults to `'3d'` (3 days).
75
75
  - `transactionLogsPath: string` The path to store transaction logs. Defaults to
76
76
  `"${db.path}/transaction_logs"`.
77
+ - `verificationTable: boolean` When `true`, this column family participates in the process-global
78
+ [Verification Table](#verification-table): transaction writes to this column family invalidate
79
+ the verification slot for each written key. Enable this only for column families whose records
80
+ are cached (e.g. the primary column family of a table). Defaults to `false`. Requires
81
+ `verificationTableEntries` to be configured before the first database is opened.
77
82
 
78
83
  ### `db.close()`
79
84
 
@@ -106,6 +111,11 @@ Sets global database settings.
106
111
  Defaults to 32MB. Set to `0` (zero) disables block cache for future opened databases. Existing
107
112
  block cache for any opened databases is resized immediately. Negative values throw an error.
108
113
  - `compactOnClose: boolean` When `true`, compacts the database on close. Defaults to `false`.
114
+ - `verificationTableEntries: number` The number of slots in the process-global
115
+ [Verification Table](#verification-table). Each slot is 8 bytes, so the default of `131072`
116
+ (128K) slots is 1 MB. Set to `0` to disable the verification table. This must be configured
117
+ before the first database is opened; once the table is materialized, attempts to change this
118
+ value throw.
109
119
  - `writeBufferManagerAllowStall: boolean` When `true`, writes are stalled once the manager's
110
120
  `buffer_size` is exceeded, providing a hard cap on memtable memory. When `false`, memtables are
111
121
  allowed to grow past the limit and flushes are simply scheduled more aggressively. Off by
@@ -305,9 +315,28 @@ assert.equal(result, 'foo');
305
315
 
306
316
  Note that all errors are returned as rejected promises.
307
317
 
318
+ See [`GetOptions`](#getoptions) for the available options.
319
+
320
+ When the `expectedVersion` option is set and the [Verification Table](#verification-table) records
321
+ a matching version for the key, `get()` returns the `FRESH_VERSION_FLAG` sentinel
322
+ (`constants.FRESH_VERSION_FLAG`) instead of reading the value — signalling that any value the
323
+ caller has already cached for this key is still fresh and no read was performed. Be sure to check
324
+ for this sentinel before treating the result as a value:
325
+
326
+ ```typescript
327
+ import { constants } from '@harperfast/rocksdb-js';
328
+
329
+ const result = db.get(key, { expectedVersion: cachedEntry.version });
330
+ if (result === constants.FRESH_VERSION_FLAG) {
331
+ // the cached value is still valid; no read occurred
332
+ return cachedEntry.value;
333
+ }
334
+ ```
335
+
308
336
  ### `db.getSync(key: Key, options?: GetOptions): any`
309
337
 
310
- Synchronous version of `get()`.
338
+ Synchronous version of `get()`. Like `get()`, this can return the `FRESH_VERSION_FLAG` sentinel when
339
+ the `expectedVersion` option is used.
311
340
 
312
341
  ### `db.getEstimatedKeyCount(): number`
313
342
 
@@ -564,6 +593,12 @@ The `attempt` parameter is the number of times the transaction has been retried.
564
593
 
565
594
  ### `TransactionOptions`
566
595
 
596
+ - `coordinatedRetry?: boolean` When `true`, an `IsBusy` conflict at commit time is retried
597
+ automatically instead of being rejected. Rather than retrying immediately and racing the
598
+ conflicting transaction again, the retry waits until the conflicting transaction has committed and
599
+ released its write intent, then re-runs the transaction body right away with no backoff delay.
600
+ Requires the column family to be opened with `verificationTable: true`. See
601
+ [Verification Table](#verification-table). Defaults to `false`.
567
602
  - `disableSnapshot?: boolean` Whether to disable snapshots. Defaults to `false`.
568
603
  - `maxRetries?: number` The maximum number of times to retry the transaction. Defaults to `3`.
569
604
  - `retryOnBusy?: boolean` Whether to retry the transaction if the commit fails with `IsBusy`.
@@ -582,6 +617,14 @@ transaction is not retried, a `ERR_TRANSACTION_ABANDONED` error will be thrown.
582
617
  Users should use the `attempt` transaction callback parameter to ensure duplicate transaction log
583
618
  entries are not added.
584
619
 
620
+ When `coordinatedRetry: true`, the retry behavior changes for `IsBusy` conflicts: instead of
621
+ rejecting (or retrying immediately and potentially conflicting again), the commit waits until the
622
+ conflicting transaction has committed and released its write intent, then re-runs the transaction
623
+ body immediately with no backoff. This is still bounded by `maxRetries` — if the transaction has
624
+ not committed after the configured number of coordinated retries, the transaction is abandoned with
625
+ an `ERR_TRANSACTION_ABANDONED` error. Coordinated retry requires the column family to be opened with
626
+ `verificationTable: true`.
627
+
585
628
  ### Class: `Transaction`
586
629
 
587
630
  The transaction callback is passed in a `Transaction` instance which contains all of the same data
@@ -953,6 +996,74 @@ Note: If the `callback` throws an error, Node.js suppress the error. Node.js 18.
953
996
  `--force-node-api-uncaught-exceptions-policy` flag which will cause errors to emit the
954
997
  `'uncaughtException'` event. Future Node.js releases will enable this flag by default.
955
998
 
999
+ ## Verification Table
1000
+
1001
+ The verification table is a process-global, fixed-size structure that lets an application cheaply
1002
+ check whether a value it has already cached is still fresh, without performing a full read. It is
1003
+ intended for read-heavy workloads where records carry a monotonically increasing numeric version.
1004
+
1005
+ Each record's version is the numeric value stored in the first 8 bytes of its value (interpreted as
1006
+ a big-endian float64). The table maps `(database, column family, key)` to a single 8-byte slot that
1007
+ holds the last-known version for that key. Because slots are addressed by a hash, distinct keys may
1008
+ share a slot; a collision only ever causes a conservative miss (a real read), never a stale value to
1009
+ be treated as fresh.
1010
+
1011
+ The freshness check works as follows:
1012
+
1013
+ 1. Pass `{ expectedVersion }` to `get()` / `getSync()`. If the slot currently records that version,
1014
+ the read is skipped and the `FRESH_VERSION_FLAG` sentinel is returned.
1015
+ 2. On a cold read, pass `{ populateVersion: true }` (or call `db.populateVersion()` afterward) to
1016
+ seed the slot with the version extracted from the value, so subsequent freshness checks hit.
1017
+
1018
+ Transaction writes to a column family opened with `verificationTable: true` invalidate the slot for
1019
+ each written key at write time, so a stale version can never survive a write. This is also what
1020
+ enables [`coordinatedRetry`](#transactionoptions): a conflicting transaction parks on the slot and
1021
+ retries once the write intent is released.
1022
+
1023
+ The table is **process-global** and backed by a single shared structure, so versions populated on
1024
+ the main thread are visible to `worker_threads` workers and vice versa.
1025
+
1026
+ ### Enabling the verification table
1027
+
1028
+ The table must be sized before the first database is opened, via the
1029
+ [`verificationTableEntries`](#dbconfigoptions) config option (default `131072` slots = 1 MB; set to
1030
+ `0` to disable). Then opt-in per column family with the `verificationTable: true` open option:
1031
+
1032
+ ```typescript
1033
+ import { RocksDatabase } from '@harperfast/rocksdb-js';
1034
+
1035
+ RocksDatabase.config({ verificationTableEntries: 128 * 1024 });
1036
+
1037
+ const db = RocksDatabase.open('path/to/db', { verificationTable: true });
1038
+ ```
1039
+
1040
+ Enable `verificationTable` only for column families whose records are cached (e.g. the primary
1041
+ column family of a table); enabling it adds per-write slot invalidation overhead.
1042
+
1043
+ ### `db.verifyVersion(key: Key, version: number): boolean`
1044
+
1045
+ Returns `true` when the verification table currently records `version` for `key` (in this database
1046
+ and column family), indicating a cached value with that version is still fresh. Returns `false`
1047
+ otherwise — including when the table is disabled. This is a cheap, synchronous check that performs
1048
+ no database read.
1049
+
1050
+ ```typescript
1051
+ if (db.verifyVersion(key, cachedEntry.version)) {
1052
+ return cachedEntry.value;
1053
+ }
1054
+ const value = db.getSync(key);
1055
+ db.populateVersion(key, extractVersion(value));
1056
+ ```
1057
+
1058
+ ### `db.populateVersion(key: Key, version: number): void`
1059
+
1060
+ Seeds the verification-table slot for `key` with `version`. This is typically called after a full
1061
+ read where the caller already knows the version. It has no effect if the slot is currently
1062
+ lock-tagged (a transaction is mid-write on that key) or if the verification table is disabled.
1063
+
1064
+ Passing `{ populateVersion: true }` to `get()` / `getSync()` performs the equivalent seeding
1065
+ automatically after a cold read, avoiding a separate call.
1066
+
956
1067
  ## Transaction Log
957
1068
 
958
1069
  A user controlled API for logging transactions. This API is designed to be generic so that you can
@@ -1011,19 +1122,34 @@ const names = db.listLogs();
1011
1122
 
1012
1123
  ### `db.purgeLogs(options?): string[]`
1013
1124
 
1125
+ ### `db.purgeLogs({ includeEntryCounts: true, ...options }): { path: string; entries: number }[]`
1126
+
1014
1127
  Deletes transaction log files older than the `transactionLogRetention` (defaults to 3 days).
1015
1128
 
1016
1129
  - `options: object`
1017
1130
  - `before?: number` Remove all transaction log files older than the specified timestamp.
1018
1131
  - `destroy?: boolean` When `true`, deletes transaction log stores including all log sequence files
1019
1132
  on disk.
1133
+ - `includeEntryCounts?: boolean` When `true`, counts the entries in each deleted log file and
1134
+ returns an array of `{ path, entries }` objects instead of an array of file paths. Counting reads
1135
+ each file before it is removed, so it is only performed when this option is enabled.
1020
1136
  - `name?: string` The name of a store to limit the purging to.
1021
1137
 
1022
- Returns an array with the full path of each log file deleted.
1138
+ The method is overloaded so the return type follows `includeEntryCounts`: by default (omitted or
1139
+ `false`) it returns `string[]` — the full path of each log file deleted; when `includeEntryCounts` is
1140
+ `true` it returns `{ path: string; entries: number }[]`, each entry being the `path` of the deleted
1141
+ log file and the number of `entries` it held. Because of the overloads, the object-array form is
1142
+ returned directly when `includeEntryCounts: true` is passed as a literal, with no casting required.
1023
1143
 
1024
1144
  ```typescript
1025
1145
  const removed = db.purgeLogs();
1026
1146
  console.log(`Removed ${removed.length} log files`);
1147
+
1148
+ // Include the entry count for each deleted log file:
1149
+ const purged = db.purgeLogs({ includeEntryCounts: true });
1150
+ for (const { path, entries } of purged) {
1151
+ console.log(`Removed ${path} (${entries} entries)`);
1152
+ }
1027
1153
  ```
1028
1154
 
1029
1155
  ### `db.useLog(name): TransactionLog`
@@ -1259,6 +1385,137 @@ import { versions } from '@harperfast/rocksdb-js';
1259
1385
  console.log(versions); // { "rocksdb": "10.10.1", "rocksdb-js": "0.1.2" }
1260
1386
  ```
1261
1387
 
1388
+ ## Checkpoints
1389
+
1390
+ ### `db.createCheckpoint(targetPath: string): Promise<void>`
1391
+
1392
+ Creates a [checkpoint](https://github.com/facebook/rocksdb/wiki/Checkpoints) — a point-in-time,
1393
+ fully independent copy of the entire database (all column families) at `targetPath` — and resolves
1394
+ once written. Unlike a backup, a checkpoint is a normal, writable database: open it as a new
1395
+ `RocksDatabase` and it diverges independently from the source.
1396
+
1397
+ SST and blob files are **hard-linked** when `targetPath` is on the **same filesystem** as the
1398
+ database, and **copied** otherwise; other files (such as the `MANIFEST`) are always copied. As a
1399
+ result the operation is near-instant on the same filesystem and as costly as a full copy across
1400
+ filesystems. The memtable is flushed so the checkpoint includes the latest writes even when the
1401
+ WAL is disabled.
1402
+
1403
+ Parent directories are created as needed. `targetPath` itself must not already exist — RocksDB
1404
+ creates the checkpoint directory — and the call rejects with `Create checkpoint failed: target
1405
+ path exists` if it does (other failures, such as a full disk, surface the RocksDB status message).
1406
+ The caller is responsible for opening the checkpoint and for eventually deleting the directory.
1407
+
1408
+ ```typescript
1409
+ const db = RocksDatabase.open('/path/to/database');
1410
+ await db.createCheckpoint('/path/to/checkpoint');
1411
+
1412
+ // The checkpoint is a normal, writable database.
1413
+ const branch = RocksDatabase.open('/path/to/checkpoint');
1414
+ ```
1415
+
1416
+ ## Backups
1417
+
1418
+ Backups use RocksDB's `BackupEngine` to capture consistent, incremental, checksum-verified
1419
+ snapshots of a database. A backup covers the **entire database** — every column family, the
1420
+ manifest, and (by default) the write-ahead log — so it is not scoped to an individual `Store`.
1421
+
1422
+ Creating a backup is an instance method (`db.backup()`) because it needs a live database. The
1423
+ remaining operations act on a backup directory and do not require an open database, so they are
1424
+ grouped under the `backups` namespace export.
1425
+
1426
+ ```typescript
1427
+ import { RocksDatabase, backups } from '@harperfast/rocksdb-js';
1428
+
1429
+ const db = RocksDatabase.open('/path/to/database');
1430
+ await db.put('foo', 'bar');
1431
+
1432
+ // Create a backup, then restore it into a fresh directory.
1433
+ const id = await db.backup('/path/to/backups');
1434
+ await backups.restore('/path/to/backups', '/path/to/restored-db');
1435
+ ```
1436
+
1437
+ ### `db.backup(backupDir: string, options?: BackupOptions): Promise<number>`
1438
+
1439
+ Creates a new backup of the entire database into `backupDir`, creating parent directories as
1440
+ needed, and resolves with the new backup id (a monotonically increasing integer). Subsequent
1441
+ backups into the same directory are incremental — unchanged immutable files are shared rather than
1442
+ re-copied.
1443
+
1444
+ When the database was opened with `disableWAL`, the memtable is flushed before the backup by
1445
+ default so unflushed data is not lost; otherwise flushing follows `options.flushBeforeBackup`.
1446
+
1447
+ ```typescript
1448
+ const id = await db.backup('/path/to/backups', { metadata: 'nightly-2026-06-04' });
1449
+ ```
1450
+
1451
+ `BackupOptions`:
1452
+
1453
+ | Option | Type | Default | Description |
1454
+ | ------------------------- | --------- | ---------------------- | --------------------------------------------------------------------- |
1455
+ | `flushBeforeBackup` | `boolean` | `true` if WAL disabled | Flush the memtable before backing up. |
1456
+ | `metadata` | `string` | `''` | Application metadata stored with the backup, returned by `list()`. |
1457
+ | `shareTableFiles` | `boolean` | `true` | Share files between backups to enable incremental backups. |
1458
+ | `shareFilesWithChecksum` | `boolean` | `true` | Distinguish shared files by checksum to avoid cross-database clashes. |
1459
+ | `backupLogFiles` | `boolean` | `true` | Include write-ahead log files in the backup. |
1460
+ | `sync` | `boolean` | `true` | `fsync` backup files for crash consistency. |
1461
+ | `maxBackgroundOperations` | `number` | `1` | Number of background threads used to copy files. |
1462
+
1463
+ ### `backups.restore(backupDir: string, dbDir: string, options?: RestoreOptions): Promise<void>`
1464
+
1465
+ Restores a backup from `backupDir` into `dbDir` (creating parent directories as needed). The
1466
+ database must **not** be open at `dbDir`, and the default restore mode is **destructive** — it
1467
+ purges `dbDir` before restoring. Restoring into the backup directory itself is rejected.
1468
+
1469
+ ```typescript
1470
+ // Restore the latest backup.
1471
+ await backups.restore('/path/to/backups', '/path/to/restored-db');
1472
+
1473
+ // Restore a specific backup id without purging matching existing files.
1474
+ await backups.restore('/path/to/backups', '/path/to/restored-db', {
1475
+ backupId: 1,
1476
+ mode: 'keepLatestDbSessionIdFiles',
1477
+ });
1478
+ ```
1479
+
1480
+ `RestoreOptions`:
1481
+
1482
+ | Option | Type | Default | Description |
1483
+ | -------------- | --------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------ |
1484
+ | `backupId` | `number` | latest backup | The backup id to restore. |
1485
+ | `walDir` | `string` | `dbDir` | Directory to restore write-ahead log files into. |
1486
+ | `keepLogFiles` | `boolean` | `false` | Keep existing log files in `walDir` rather than overwriting. |
1487
+ | `mode` | `'purgeAllFiles' \| 'keepLatestDbSessionIdFiles' \| 'verifyChecksum'` | `'purgeAllFiles'` | The restore strategy (default purges the destination). |
1488
+
1489
+ ### `backups.list(backupDir: string): Promise<BackupInfo[]>`
1490
+
1491
+ Lists the non-corrupt backups in `backupDir`, ordered by id.
1492
+
1493
+ ```typescript
1494
+ const list = await backups.list('/path/to/backups');
1495
+ // [{ backupId: 1, timestamp: 1749000000, size: 4096, numberFiles: 3, appMetadata: '' }, ...]
1496
+ ```
1497
+
1498
+ Each `BackupInfo` contains `backupId`, `timestamp` (seconds since the epoch), `size` (bytes),
1499
+ `numberFiles`, and `appMetadata`.
1500
+
1501
+ ### `backups.delete(backupDir: string, backupId: number): Promise<void>`
1502
+
1503
+ Deletes a specific backup. Shared files are reference-counted and only removed once no remaining
1504
+ backup references them, so this is not equivalent to deleting files manually.
1505
+
1506
+ ### `backups.purge(backupDir: string, keepCount: number): Promise<void>`
1507
+
1508
+ Deletes all but the newest `keepCount` backups.
1509
+
1510
+ ### `backups.verify(backupDir: string, backupId: number, options?: { verifyWithChecksum?: boolean }): Promise<void>`
1511
+
1512
+ Verifies a backup's file sizes, and optionally their checksums (which requires reading all
1513
+ backed-up data). Resolves if the backup is intact and rejects otherwise.
1514
+
1515
+ ```typescript
1516
+ await backups.verify('/path/to/backups', 1, { verifyWithChecksum: true });
1517
+ ```
1518
+
1262
1519
  ## Custom Store
1263
1520
 
1264
1521
  The store is a class that sits between the `RocksDatabase` or `Transaction` instance and the native
@@ -1275,7 +1532,7 @@ The default `Store` contains the following methods which can be overridden:
1275
1532
  - `decodeValue(value)`
1276
1533
  - `encodeKey(key)`
1277
1534
  - `encodeValue(value)`
1278
- - `get(context, key, resolve, reject, txnId?)`
1535
+ - `get(context, key, alwaysCreateNewBuffer?, options?)`
1279
1536
  - `getCount(context, options?, txnId?)`
1280
1537
  - `getKeys(context, options?)`
1281
1538
  - `getKeysCount(context, options?)`
@@ -1286,11 +1543,13 @@ The default `Store` contains the following methods which can be overridden:
1286
1543
  - `isOpen()`
1287
1544
  - `listLogs()`
1288
1545
  - `open()`
1546
+ - `populateVersion(key, version)`
1289
1547
  - `putSync(context, key, value, options?)`
1290
1548
  - `removeSync(context, key, options?)`
1291
1549
  - `tryLock(key, onUnlocked?)`
1292
1550
  - `unlock(key)`
1293
1551
  - `useLog(context, name)`
1552
+ - `verifyVersion(key, version)`
1294
1553
  - `withLock(key, callback?)`
1295
1554
 
1296
1555
  To use it, extend the default `Store` and pass in an instance of your store into the `RocksDatabase`
@@ -1300,15 +1559,15 @@ constructor.
1300
1559
  import { RocksDatabase, Store } from '@harperfast/rocksdb-js';
1301
1560
 
1302
1561
  class MyStore extends Store {
1303
- get(context, key, resolve, reject, txnId) {
1304
- console.log('Getting:' key);
1305
- return super.get(context, key, resolve, reject, txnId);
1306
- }
1307
-
1308
- putSync(context, key, value, options) {
1309
- console.log('Putting:', key);
1310
- return super.putSync(context, key, value, options);
1311
- }
1562
+ get(context, key, alwaysCreateNewBuffer, options) {
1563
+ console.log('Getting:', key);
1564
+ return super.get(context, key, alwaysCreateNewBuffer, options);
1565
+ }
1566
+
1567
+ putSync(context, key, value, options) {
1568
+ console.log('Putting:', key);
1569
+ return super.putSync(context, key, value, options);
1570
+ }
1312
1571
  }
1313
1572
 
1314
1573
  const myStore = new MyStore('path/to/db');
@@ -1328,6 +1587,22 @@ console.log(await db.get('foo'));
1328
1587
 
1329
1588
  ## Interfaces
1330
1589
 
1590
+ ### `GetOptions`
1591
+
1592
+ Options for `get()`, `getSync()`, and the `getBinary*` methods.
1593
+
1594
+ - `options: object`
1595
+ - `expectedVersion: number` When set, the [Verification Table](#verification-table) is checked
1596
+ before reading. If the slot holds this version, the read is skipped and the
1597
+ `FRESH_VERSION_FLAG` sentinel is returned. After a database read, the slot is seeded with the
1598
+ version extracted from the value. Requires the column family to be opened with
1599
+ `verificationTable: true`.
1600
+ - `populateVersion: boolean` When `true`, after a database read the verification-table slot is
1601
+ seeded with the version extracted from the value, eliminating the need for a separate
1602
+ `db.populateVersion()` call on cold reads. Defaults to `false`.
1603
+ - `skipDecode: boolean` When `true`, the value is returned without being decoded. Defaults to
1604
+ `false`.
1605
+
1331
1606
  ### `RocksDBOptions`
1332
1607
 
1333
1608
  - `options: object`
@@ -1393,6 +1668,11 @@ Options:
1393
1668
 
1394
1669
  Available commands:
1395
1670
 
1671
+ - `backups <dir> [subcommand]` Manage database backups; with no subcommand or `ls`/`list`, lists the backups in
1672
+ `<dir>`. Subcommands: `backup` (create a backup of the open database), `restore <backup-id>`
1673
+ (restore into the open database after confirmation; unavailable in read-only mode),
1674
+ `verify <backup-id>` (checksum verification), `delete <backup-id>`, and `purge <keep-count>`
1675
+ (delete all but the newest backups). `delete` and `purge` report the recovered disk space.
1396
1676
  - `clear` Clear all data in the current column family
1397
1677
  - `columns` List column families
1398
1678
  - `compact` Compact the current column family