@harperfast/rocksdb-js 0.1.11 → 0.1.13

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
@@ -396,7 +396,7 @@ Synchronous version of `remove()`.
396
396
 
397
397
  ## Transactions
398
398
 
399
- ### `db.transaction(async (txn: Transaction) => void | Promise<any>): Promise<any>`
399
+ ### `db.transaction<T>(callback: TransactionCallback<T>, options?: DBTransactionOptions): Promise<T>`
400
400
 
401
401
  Executes all database operations within the specified callback within a single transaction. If the
402
402
  callback completes without error, the database operations are automatically committed. However, if
@@ -427,7 +427,7 @@ const isBar = await db.transaction(async (txn: Transaction) => {
427
427
  console.log(isBar ? 'Foo is bar' : 'Foo is not bar');
428
428
  ```
429
429
 
430
- ### `db.transactionSync((txn: Transaction) => any): any`
430
+ ### `db.transactionSync<T>(callback: TransactionCallback<T>, options?: TransactionOptions): T`
431
431
 
432
432
  Executes a transaction callback and commits synchronously. Once the transaction callback returns,
433
433
  the commit is executed synchronously and blocks the current thread until finished.
@@ -441,17 +441,56 @@ db.transactionSync((txn: Transaction) => {
441
441
  });
442
442
  ```
443
443
 
444
+ ### `TransactionCallback<T>`
445
+
446
+ `(txn: Transaction, attempt: number) => T | PromiseLike<T>`
447
+
448
+ A sync or async function to encapsulate all of the transaction operations. Once the function is
449
+ executed, the transaction is automatically committed. If the function returns a value, it will be
450
+ returned from the transaction call.
451
+
452
+ The `txn` parameter is a `Transaction`. See the [Transaction](#class-transaction) section for more
453
+ details.
454
+
455
+ The `attempt` parameter is the number of times the transaction has been retried.
456
+
457
+ ### `TransactionOptions`
458
+
459
+ - `disableSnapshot?: boolean` Whether to disable snapshots. Defaults to `false`.
460
+ - `maxRetries?: number` The maximum number of times to retry the transaction. Defaults to `3`.
461
+ - `retryOnBusy?: boolean` Whether to retry the transaction if the commit fails with `IsBusy`.
462
+ Defaults to `true` when the transaction is bound to a transaction log, otherwise `false`.
463
+
464
+ ### Transaction Retry Logic
465
+
466
+ The retry mechanism will only be active when the `retryOnBusy` option is `true` or when
467
+ `retryOnBusy` is `undefined` and the transaction is bound to a transaction log. The attempts starts
468
+ at `1` and ends at `maxRetries`.
469
+
470
+ When using a transaction log and the commit fails with `ERR_BUSY`, the transaction log will be in
471
+ a bad state and the transaction will need to be retried. If the max retries is reached or the
472
+ transaction is not retried, a `ERR_TRANSACTION_ABANDONED` error will be thrown.
473
+
474
+ Users should use the `attempt` transaction callback parameter to ensure duplicate transaction log
475
+ entries are not added.
476
+
444
477
  ### Class: `Transaction`
445
478
 
446
479
  The transaction callback is passed in a `Transaction` instance which contains all of the same data
447
480
  operations methods as the `RocksDatabase` instance plus:
448
481
 
449
- - `txn.abort()`
450
- - `txn.commit()`
451
- - `txn.commitSync()`
452
- - `txn.getTimestamp()`
453
- - `txn.id`
454
- - `txn.setTimestamp(ts)`
482
+ - `txn.abort()` Rolls back and closes the transaction. This method is automatically called after the
483
+ transaction callback returns, so you shouldn't need to call it, but it's ok to do so. Once called,
484
+ no further transaction operations are permitted. Calling this method multiple times has no effect.
485
+ - `txn.commit(): Promise<void>` Asynchronously commits the transaction and closes the transaction.
486
+ - `txn.commitSync()` Synchronously commits and closes the transaction.
487
+ - `txn.getTimestamp(): number` Retrieves the transaction start timestamp in seconds as a decimal. It
488
+ defaults to the time at which the transaction was created.
489
+ - `txn.id: number` The readonly transaction ID. Transaction IDs are unique to the RocksDB database
490
+ path, regardless the database name/column family.
491
+ - `txn.setTimestamp(ts?: number): void` Overrides the transaction start timestamp. If called without
492
+ a timestamp, it will set the timestamp to the current time. The value must be in seconds with
493
+ higher precision in the decimal.
455
494
 
456
495
  #### `txn.abort(): void`
457
496
 
package/dist/index.cjs CHANGED
@@ -1011,6 +1011,29 @@ function getKeyParam(keyBuffer) {
1011
1011
 
1012
1012
  //#endregion
1013
1013
  //#region src/transaction.ts
1014
+ var TransactionAlreadyAbortedError = class extends Error {
1015
+ code = "ERR_ALREADY_ABORTED";
1016
+ };
1017
+ var TransactionIsBusyError = class extends Error {
1018
+ code = "ERR_BUSY";
1019
+ hasLog;
1020
+ txn;
1021
+ constructor(error, txn) {
1022
+ super(error.message);
1023
+ this.hasLog = error.hasLog ?? false;
1024
+ this.txn = txn;
1025
+ }
1026
+ };
1027
+ var TransactionAbandonedError = class extends Error {
1028
+ code = "ERR_TRANSACTION_ABANDONED";
1029
+ hasLog;
1030
+ txn;
1031
+ constructor(error, txn) {
1032
+ super(error.message);
1033
+ this.hasLog = error.hasLog ?? false;
1034
+ this.txn = txn;
1035
+ }
1036
+ };
1014
1037
  /**
1015
1038
  * Provides transaction level operations to a transaction callback.
1016
1039
  */
@@ -1031,7 +1054,12 @@ var Transaction = class extends DBI {
1031
1054
  * Abort the transaction.
1032
1055
  */
1033
1056
  abort() {
1034
- this.#txn.abort();
1057
+ try {
1058
+ this.#txn.abort();
1059
+ } catch (err) {
1060
+ if (err instanceof Error && "code" in err && err.code === "ERR_TRANSACTION_ABANDONED") throw new TransactionAbandonedError(err, this);
1061
+ throw err;
1062
+ }
1035
1063
  }
1036
1064
  /**
1037
1065
  * Commit the transaction.
@@ -1042,6 +1070,8 @@ var Transaction = class extends DBI {
1042
1070
  this.notify("beforecommit");
1043
1071
  this.#txn.commit(resolve, reject);
1044
1072
  });
1073
+ } catch (err) {
1074
+ throw this.#handleCommitError(err);
1045
1075
  } finally {
1046
1076
  this.notify("aftercommit", {
1047
1077
  next: null,
@@ -1057,6 +1087,8 @@ var Transaction = class extends DBI {
1057
1087
  try {
1058
1088
  this.notify("beforecommit");
1059
1089
  this.#txn.commitSync();
1090
+ } catch (err) {
1091
+ throw this.#handleCommitError(err);
1060
1092
  } finally {
1061
1093
  this.notify("aftercommit", {
1062
1094
  next: null,
@@ -1066,6 +1098,19 @@ var Transaction = class extends DBI {
1066
1098
  }
1067
1099
  }
1068
1100
  /**
1101
+ * Detect if error is an already aborted or busy error and return the appropriate error class.
1102
+ *
1103
+ * @param err - The error to check.
1104
+ * @returns The specialized error.
1105
+ */
1106
+ #handleCommitError(err) {
1107
+ if (err instanceof Error && "code" in err) {
1108
+ if (err.code === "ERR_ALREADY_ABORTED") return new TransactionAlreadyAbortedError(err.message);
1109
+ if (err.code === "ERR_BUSY") return new TransactionIsBusyError(err, this);
1110
+ }
1111
+ return err;
1112
+ }
1113
+ /**
1069
1114
  * Returns the transaction start timestamp in seconds. Defaults to the time at which
1070
1115
  * the transaction was created.
1071
1116
  *
@@ -1472,25 +1517,24 @@ var RocksDatabase = class RocksDatabase extends DBI {
1472
1517
  */
1473
1518
  async transaction(callback, options) {
1474
1519
  if (typeof callback !== "function") throw new TypeError("Callback must be a function");
1520
+ const maxRetries = options?.maxRetries ?? 3;
1475
1521
  const txn = new Transaction(this.store, options);
1476
1522
  let result;
1477
- try {
1478
- this.notify("begin-transaction");
1479
- result = await callback(txn);
1480
- } catch (err) {
1523
+ this.notify("begin-transaction");
1524
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
1481
1525
  try {
1482
- txn.abort();
1483
- } catch (err) {
1484
- if (err instanceof Error && "code" in err && err.code === "ERR_ALREADY_ABORTED") return;
1526
+ result = await callback(txn, attempt);
1527
+ } catch (callbackErr) {
1528
+ return this.#abortTransaction(txn, callbackErr);
1529
+ }
1530
+ try {
1531
+ await txn.commit();
1532
+ return result;
1533
+ } catch (commitErr) {
1534
+ if (commitErr instanceof TransactionAlreadyAbortedError) return;
1535
+ if (commitErr instanceof TransactionIsBusyError && (options?.retryOnBusy ?? commitErr.hasLog) && attempt <= maxRetries) continue;
1536
+ this.#abandonTransaction(txn, commitErr);
1485
1537
  }
1486
- throw err;
1487
- }
1488
- try {
1489
- await txn.commit();
1490
- return result;
1491
- } catch (err) {
1492
- if (err instanceof Error && "code" in err && err.code === "ERR_ALREADY_ABORTED") return;
1493
- throw err;
1494
1538
  }
1495
1539
  }
1496
1540
  /**
@@ -1512,38 +1556,55 @@ var RocksDatabase = class RocksDatabase extends DBI {
1512
1556
  */
1513
1557
  transactionSync(callback, options) {
1514
1558
  if (typeof callback !== "function") throw new TypeError("Callback must be a function");
1515
- const txn = new Transaction(this.store, options);
1516
- let result;
1517
- try {
1518
- this.notify("begin-transaction");
1519
- result = callback(txn);
1520
- } catch (err) {
1559
+ const maxRetries = options?.maxRetries ?? 3;
1560
+ const isRetryable = (err, attempt) => {
1561
+ return err instanceof TransactionIsBusyError && (options?.retryOnBusy ?? err.hasLog) && attempt <= maxRetries;
1562
+ };
1563
+ const runAttempt = (attempt) => {
1564
+ const txn = new Transaction(this.store, options);
1565
+ let result;
1521
1566
  try {
1522
- txn.abort();
1523
- } catch (err) {
1524
- if (err instanceof Error && "code" in err && err.code === "ERR_ALREADY_ABORTED") return;
1567
+ result = callback(txn, attempt);
1568
+ } catch (callbackErr) {
1569
+ return this.#abortTransaction(txn, callbackErr);
1525
1570
  }
1526
- throw err;
1527
- }
1528
- if (result && typeof result === "object" && "then" in result && typeof result.then === "function") return result.then((value) => {
1571
+ if (typeof result?.then === "function") return result.then((value) => {
1572
+ try {
1573
+ txn.commitSync();
1574
+ return value;
1575
+ } catch (commitErr) {
1576
+ if (commitErr instanceof TransactionAlreadyAbortedError) return;
1577
+ if (isRetryable(commitErr, attempt)) return runAttempt(attempt + 1);
1578
+ this.#abandonTransaction(txn, commitErr);
1579
+ }
1580
+ });
1529
1581
  try {
1530
1582
  txn.commitSync();
1531
- return value;
1532
- } catch (err) {
1533
- if (err instanceof Error && "code" in err && err.code === "ERR_ALREADY_ABORTED") return;
1534
- throw err;
1583
+ return result;
1584
+ } catch (commitErr) {
1585
+ if (commitErr instanceof TransactionAlreadyAbortedError) return;
1586
+ if (isRetryable(commitErr, attempt)) return runAttempt(attempt + 1);
1587
+ this.#abandonTransaction(txn, commitErr);
1535
1588
  }
1536
- });
1589
+ };
1590
+ this.notify("begin-transaction");
1591
+ return runAttempt(1);
1592
+ }
1593
+ #abortTransaction(txn, callbackErr) {
1537
1594
  try {
1538
- txn.commitSync();
1539
- return result;
1540
- } catch (err) {
1541
- if (err instanceof Error && "code" in err && err.code === "ERR_ALREADY_ABORTED") return;
1542
- try {
1543
- txn.abort();
1544
- } catch {}
1545
- throw err;
1595
+ txn.abort();
1596
+ } catch (abortErr) {
1597
+ if (abortErr instanceof TransactionAlreadyAbortedError) return;
1598
+ }
1599
+ throw callbackErr;
1600
+ }
1601
+ #abandonTransaction(txn, commitErr) {
1602
+ try {
1603
+ txn.abort();
1604
+ } catch (abortErr) {
1605
+ if (abortErr instanceof TransactionAbandonedError) throw abortErr;
1546
1606
  }
1607
+ throw commitErr;
1547
1608
  }
1548
1609
  /**
1549
1610
  * Attempts to acquire a lock for a given key. If the lock is available,
@@ -1700,6 +1761,7 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
1700
1761
  let foundExactStart = false;
1701
1762
  if (start === void 0 && !startFromLastFlushed) {
1702
1763
  position = size;
1764
+ if (position === 0) position = TRANSACTION_LOG_FILE_HEADER_SIZE;
1703
1765
  start = 0;
1704
1766
  } else {
1705
1767
  if (startFromLastFlushed) {
@@ -1709,6 +1771,7 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
1709
1771
  } else FLOAT_TO_UINT32[0] = this._findPosition(start);
1710
1772
  logId = UINT32_FROM_FLOAT[1];
1711
1773
  position = UINT32_FROM_FLOAT[0];
1774
+ if (position === 0) position = TRANSACTION_LOG_FILE_HEADER_SIZE;
1712
1775
  }
1713
1776
  if (logBuffer === void 0 || logBuffer.logId !== logId) {
1714
1777
  logBuffer = getLogMemoryMap(this, logId);
@@ -1845,7 +1908,7 @@ function loadLastPosition(transactionLog, readUncommitted) {
1845
1908
  //#region src/index.ts
1846
1909
  const versions = {
1847
1910
  rocksdb: version,
1848
- "rocksdb-js": "0.1.11"
1911
+ "rocksdb-js": "0.1.13"
1849
1912
  };
1850
1913
 
1851
1914
  //#endregion