serializable-bptree 7.0.2 → 7.0.4

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.
@@ -475,24 +475,13 @@ var BPTree = class _BPTree {
475
475
  option;
476
476
  order;
477
477
  rootId;
478
- /**
479
- * Returns the ID of the root node.
480
- * @returns The root node ID.
481
- */
482
- getRootId() {
483
- return this.rootId;
484
- }
485
- /**
486
- * Returns the order of the B+Tree.
487
- * @returns The order of the tree.
488
- */
489
- getOrder() {
490
- return this.order;
491
- }
492
478
  _strategyDirty;
493
479
  _nodeCreateBuffer;
494
480
  _nodeUpdateBuffer;
495
481
  _nodeDeleteBuffer;
482
+ sharedDeleteCache = /* @__PURE__ */ new Map();
483
+ activeTransactions = /* @__PURE__ */ new Set();
484
+ lastTransactionId = 0;
496
485
  verifierMap = {
497
486
  gt: (nv, v) => this.comparator.isHigher(nv, v),
498
487
  gte: (nv, v) => this.comparator.isHigher(nv, v) || this.comparator.isSame(nv, v),
@@ -653,6 +642,20 @@ var BPTree = class _BPTree {
653
642
  }
654
643
  return best;
655
644
  }
645
+ /**
646
+ * Returns the ID of the root node.
647
+ * @returns The root node ID.
648
+ */
649
+ getRootId() {
650
+ return this.rootId;
651
+ }
652
+ /**
653
+ * Returns the order of the B+Tree.
654
+ * @returns The order of the tree.
655
+ */
656
+ getOrder() {
657
+ return this.order;
658
+ }
656
659
  /**
657
660
  * Verified if the value satisfies the condition.
658
661
  *
@@ -772,6 +775,38 @@ var BPTree = class _BPTree {
772
775
  this._cachedRegexp.clear();
773
776
  this.nodes.clear();
774
777
  }
778
+ registerTransaction(txId) {
779
+ this.activeTransactions.add(txId);
780
+ }
781
+ unregisterTransaction(txId) {
782
+ this.activeTransactions.delete(txId);
783
+ }
784
+ pruneObsoleteNodes() {
785
+ if (this.activeTransactions.size === 0) {
786
+ this.sharedDeleteCache.clear();
787
+ return;
788
+ }
789
+ const minActiveTxId = Math.min(...this.activeTransactions);
790
+ for (const [id, entry] of this.sharedDeleteCache) {
791
+ if (entry.obsoleteAt < minActiveTxId) {
792
+ this.sharedDeleteCache.delete(id);
793
+ }
794
+ }
795
+ }
796
+ getObsoleteNode(id) {
797
+ return this.sharedDeleteCache.get(id)?.node;
798
+ }
799
+ addObsoleteNode(node, obsoleteAt) {
800
+ this.sharedDeleteCache.set(node.id, { node, obsoleteAt });
801
+ }
802
+ getNextTransactionId() {
803
+ let nextId = Date.now();
804
+ if (nextId <= this.lastTransactionId) {
805
+ nextId = this.lastTransactionId + 1e-3;
806
+ }
807
+ this.lastTransactionId = nextId;
808
+ return nextId;
809
+ }
775
810
  };
776
811
 
777
812
  // src/base/BPTreeSyncBase.ts
@@ -1360,7 +1395,9 @@ var BPTreeSyncBase = class extends BPTree {
1360
1395
  this._nodeUpdateBuffer.clear();
1361
1396
  }
1362
1397
  commitNodeDeleteBuffer() {
1398
+ const obsoleteAt = this.getNextTransactionId();
1363
1399
  for (const node of this._nodeDeleteBuffer.values()) {
1400
+ this.addObsoleteNode(node, obsoleteAt);
1364
1401
  this.strategy.delete(node.id);
1365
1402
  this.nodes.delete(node.id);
1366
1403
  }
@@ -1522,6 +1559,7 @@ var BPTreeSyncBase = class extends BPTree {
1522
1559
  var SerializeStrategy = class {
1523
1560
  order;
1524
1561
  head;
1562
+ lastCommittedTransactionId = 0;
1525
1563
  constructor(order) {
1526
1564
  this.order = order;
1527
1565
  this.head = {
@@ -1550,13 +1588,13 @@ var SerializeStrategySync = class extends SerializeStrategy {
1550
1588
  this.setHeadData(key, next);
1551
1589
  return current;
1552
1590
  }
1553
- compareAndSwapHead(oldRoot, newRoot) {
1554
- if (this.head.root !== oldRoot) {
1555
- return false;
1556
- }
1591
+ getLastCommittedTransactionId() {
1592
+ return this.lastCommittedTransactionId;
1593
+ }
1594
+ compareAndSwapHead(newRoot, newTxId) {
1557
1595
  this.head.root = newRoot;
1596
+ this.lastCommittedTransactionId = newTxId;
1558
1597
  this.writeHead(this.head);
1559
- return true;
1560
1598
  }
1561
1599
  };
1562
1600
  var InMemoryStoreStrategySync = class extends SerializeStrategySync {
@@ -1625,8 +1663,11 @@ var BPTreeSyncSnapshotStrategy = class extends SerializeStrategySync {
1625
1663
  this.snapshotHead.root = head.root;
1626
1664
  this.snapshotHead.data = { ...head.data };
1627
1665
  }
1628
- compareAndSwapHead(oldRoot, newRoot) {
1629
- return this.baseStrategy.compareAndSwapHead(oldRoot, newRoot);
1666
+ compareAndSwapHead(newRoot, newTxId) {
1667
+ this.baseStrategy.compareAndSwapHead(newRoot, newTxId);
1668
+ }
1669
+ getLastCommittedTransactionId() {
1670
+ return this.baseStrategy.getLastCommittedTransactionId();
1630
1671
  }
1631
1672
  getHeadData(key, defaultValue) {
1632
1673
  return this.snapshotHead.data[key] ?? defaultValue;
@@ -1647,8 +1688,11 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1647
1688
  dirtyIds;
1648
1689
  createdInTx;
1649
1690
  deletedIds;
1691
+ originalNodes = /* @__PURE__ */ new Map();
1650
1692
  initialRootId;
1651
1693
  transactionRootId;
1694
+ transactionId;
1695
+ initialLastCommittedTransactionId = 0;
1652
1696
  constructor(baseTree) {
1653
1697
  super(baseTree.strategy, baseTree.comparator, baseTree.option);
1654
1698
  this.realBaseTree = baseTree;
@@ -1659,6 +1703,7 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1659
1703
  this.dirtyIds = /* @__PURE__ */ new Set();
1660
1704
  this.createdInTx = /* @__PURE__ */ new Set();
1661
1705
  this.deletedIds = /* @__PURE__ */ new Set();
1706
+ this.transactionId = Date.now() + Math.random();
1662
1707
  }
1663
1708
  /**
1664
1709
  * Initializes the transaction by capturing the current state of the tree.
@@ -1675,6 +1720,7 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1675
1720
  const root = this._createNode(true, [], [], true);
1676
1721
  this.initialRootId = root.id;
1677
1722
  }
1723
+ this.initialLastCommittedTransactionId = this.realBaseStrategy.getLastCommittedTransactionId();
1678
1724
  this.transactionRootId = this.initialRootId;
1679
1725
  this.rootId = this.transactionRootId;
1680
1726
  const snapshotStrategy = new BPTreeSyncSnapshotStrategy(this.realBaseStrategy, this.initialRootId);
@@ -1683,6 +1729,7 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1683
1729
  this.dirtyIds.clear();
1684
1730
  this.createdInTx.clear();
1685
1731
  this.deletedIds.clear();
1732
+ this.realBaseTree.registerTransaction(this.transactionId);
1686
1733
  }
1687
1734
  getNode(id) {
1688
1735
  if (this.txNodes.has(id)) {
@@ -1691,7 +1738,13 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1691
1738
  if (this.deletedIds.has(id)) {
1692
1739
  throw new Error(`The tree attempted to reference deleted node '${id}'`);
1693
1740
  }
1694
- const baseNode = this.realBaseStrategy.read(id);
1741
+ let baseNode = this.realBaseTree.getObsoleteNode(id);
1742
+ if (!baseNode) {
1743
+ baseNode = this.realBaseStrategy.read(id);
1744
+ }
1745
+ if (!this.originalNodes.has(id) && !this.createdInTx.has(id)) {
1746
+ this.originalNodes.set(id, JSON.parse(JSON.stringify(baseNode)));
1747
+ }
1695
1748
  const clone = JSON.parse(JSON.stringify(baseNode));
1696
1749
  this.txNodes.set(id, clone);
1697
1750
  return clone;
@@ -1781,9 +1834,10 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1781
1834
  * Attempts to commit the transaction.
1782
1835
  * Uses Optimistic Locking (Compare-And-Swap) on the root node ID to detect conflicts.
1783
1836
  *
1837
+ * @param cleanup Whether to clean up obsolete nodes after commit. Defaults to true.
1784
1838
  * @returns The transaction result.
1785
1839
  */
1786
- commit() {
1840
+ commit(cleanup = true) {
1787
1841
  const idMapping = /* @__PURE__ */ new Map();
1788
1842
  const finalNodes = [];
1789
1843
  for (const oldId of this.dirtyIds) {
@@ -1831,24 +1885,46 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1831
1885
  if (idMapping.has(this.rootId)) {
1832
1886
  newRootId = idMapping.get(this.rootId);
1833
1887
  }
1834
- for (const node of finalNodes) {
1835
- this.realBaseStrategy.write(node.id, node);
1888
+ let success = false;
1889
+ if (finalNodes.length === 0) {
1890
+ success = true;
1891
+ } else if (this.realBaseStrategy.getLastCommittedTransactionId() === this.initialLastCommittedTransactionId) {
1892
+ for (const node of finalNodes) {
1893
+ this.realBaseStrategy.write(node.id, node);
1894
+ }
1895
+ this.realBaseStrategy.compareAndSwapHead(newRootId, this.transactionId);
1896
+ success = true;
1836
1897
  }
1837
- const success = this.realBaseStrategy.compareAndSwapHead(this.initialRootId, newRootId);
1838
1898
  if (success) {
1839
1899
  const distinctObsolete = /* @__PURE__ */ new Set();
1840
1900
  for (const oldId of this.dirtyIds) {
1841
- if (!this.createdInTx.has(oldId) && this.txNodes.has(oldId)) {
1901
+ if (this.createdInTx.has(oldId)) {
1902
+ continue;
1903
+ }
1904
+ if (this.txNodes.has(oldId) || this.deletedIds.has(oldId)) {
1842
1905
  distinctObsolete.add(oldId);
1843
1906
  }
1844
1907
  }
1908
+ if (cleanup) {
1909
+ for (const obsoleteId of distinctObsolete) {
1910
+ if (this.originalNodes.has(obsoleteId)) {
1911
+ this.realBaseTree.addObsoleteNode(
1912
+ this.originalNodes.get(obsoleteId),
1913
+ this.transactionId
1914
+ );
1915
+ }
1916
+ this.realBaseStrategy.delete(obsoleteId);
1917
+ }
1918
+ }
1919
+ this.realBaseTree.unregisterTransaction(this.transactionId);
1920
+ this.realBaseTree.pruneObsoleteNodes();
1845
1921
  return {
1846
1922
  success: true,
1847
1923
  createdIds: newCreatedIds,
1848
1924
  obsoleteIds: Array.from(distinctObsolete)
1849
1925
  };
1850
1926
  } else {
1851
- this.rollback();
1927
+ this.rollback(cleanup);
1852
1928
  return {
1853
1929
  success: false,
1854
1930
  createdIds: newCreatedIds,
@@ -1872,8 +1948,16 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1872
1948
  this.realBaseStrategy.delete(id);
1873
1949
  }
1874
1950
  }
1951
+ this.realBaseTree.unregisterTransaction(this.transactionId);
1952
+ this.realBaseTree.pruneObsoleteNodes();
1875
1953
  return createdIds;
1876
1954
  }
1955
+ readLock(fn) {
1956
+ return fn();
1957
+ }
1958
+ writeLock(fn) {
1959
+ return fn();
1960
+ }
1877
1961
  // Override to do nothing, as transaction handles its own commits
1878
1962
  commitHeadBuffer() {
1879
1963
  }
@@ -2511,7 +2595,9 @@ var BPTreeAsyncBase = class extends BPTree {
2511
2595
  this._nodeUpdateBuffer.clear();
2512
2596
  }
2513
2597
  async commitNodeDeleteBuffer() {
2598
+ const obsoleteAt = this.getNextTransactionId();
2514
2599
  for (const node of this._nodeDeleteBuffer.values()) {
2600
+ this.addObsoleteNode(node, obsoleteAt);
2515
2601
  await this.strategy.delete(node.id);
2516
2602
  this.nodes.delete(node.id);
2517
2603
  }
@@ -2676,8 +2762,276 @@ var BPTreeAsyncBase = class extends BPTree {
2676
2762
  }
2677
2763
  };
2678
2764
 
2765
+ // node_modules/ryoiki/dist/esm/index.mjs
2766
+ var Ryoiki = class _Ryoiki {
2767
+ readings;
2768
+ writings;
2769
+ readQueue;
2770
+ writeQueue;
2771
+ static async CatchError(promise) {
2772
+ return await promise.then((v) => [void 0, v]).catch((err) => [err]);
2773
+ }
2774
+ static IsRangeOverlap(a, b) {
2775
+ const [start1, end1] = a;
2776
+ const [start2, end2] = b;
2777
+ if (end1 <= start2 || end2 <= start1) {
2778
+ return false;
2779
+ }
2780
+ return true;
2781
+ }
2782
+ static ERR_ALREADY_EXISTS(lockId) {
2783
+ return new Error(`The '${lockId}' task already existing in queue or running.`);
2784
+ }
2785
+ static ERR_NOT_EXISTS(lockId) {
2786
+ return new Error(`The '${lockId}' task not existing in task queue.`);
2787
+ }
2788
+ static ERR_TIMEOUT(lockId, timeout) {
2789
+ return new Error(`The task with ID '${lockId}' failed to acquire the lock within the timeout(${timeout}ms).`);
2790
+ }
2791
+ /**
2792
+ * Constructs a new instance of the Ryoiki class.
2793
+ */
2794
+ constructor() {
2795
+ this.readings = /* @__PURE__ */ new Map();
2796
+ this.writings = /* @__PURE__ */ new Map();
2797
+ this.readQueue = /* @__PURE__ */ new Map();
2798
+ this.writeQueue = /* @__PURE__ */ new Map();
2799
+ }
2800
+ /**
2801
+ * Creates a range based on a start value and length.
2802
+ * @param start - The starting value of the range.
2803
+ * @param length - The length of the range.
2804
+ * @returns A range tuple [start, start + length].
2805
+ */
2806
+ range(start, length) {
2807
+ return [start, start + length];
2808
+ }
2809
+ rangeOverlapping(tasks, range) {
2810
+ return Array.from(tasks.values()).some((t) => _Ryoiki.IsRangeOverlap(t.range, range));
2811
+ }
2812
+ isSameRange(a, b) {
2813
+ const [a1, a2] = a;
2814
+ const [b1, b2] = b;
2815
+ return a1 === b1 && a2 === b2;
2816
+ }
2817
+ fetchUnitAndRun(queue, workspaces) {
2818
+ for (const [id, unit] of queue) {
2819
+ if (!unit.condition()) {
2820
+ continue;
2821
+ }
2822
+ this._alloc(queue, workspaces, id);
2823
+ }
2824
+ }
2825
+ _handleOverload(args, handlers, argPatterns) {
2826
+ for (const [key, pattern] of Object.entries(argPatterns)) {
2827
+ if (this._matchArgs(args, pattern)) {
2828
+ return handlers[key](...args);
2829
+ }
2830
+ }
2831
+ throw new Error("Invalid arguments");
2832
+ }
2833
+ _matchArgs(args, pattern) {
2834
+ return args.every((arg, index) => {
2835
+ const expectedType = pattern[index];
2836
+ if (expectedType === void 0) return typeof arg === "undefined";
2837
+ if (expectedType === Function) return typeof arg === "function";
2838
+ if (expectedType === Number) return typeof arg === "number";
2839
+ if (expectedType === Array) return Array.isArray(arg);
2840
+ return false;
2841
+ });
2842
+ }
2843
+ _createRandomId() {
2844
+ const timestamp = Date.now().toString(36);
2845
+ const random = Math.random().toString(36).substring(2);
2846
+ return `${timestamp}${random}`;
2847
+ }
2848
+ _alloc(queue, workspaces, lockId) {
2849
+ const unit = queue.get(lockId);
2850
+ if (!unit) {
2851
+ throw _Ryoiki.ERR_NOT_EXISTS(lockId);
2852
+ }
2853
+ workspaces.set(lockId, unit);
2854
+ queue.delete(lockId);
2855
+ unit.alloc();
2856
+ }
2857
+ _free(workspaces, lockId) {
2858
+ const unit = workspaces.get(lockId);
2859
+ if (!unit) {
2860
+ throw _Ryoiki.ERR_NOT_EXISTS(lockId);
2861
+ }
2862
+ workspaces.delete(lockId);
2863
+ unit.free();
2864
+ }
2865
+ _lock(queue, range, timeout, task, condition) {
2866
+ return new Promise((resolve, reject) => {
2867
+ let timeoutId = null;
2868
+ if (timeout >= 0) {
2869
+ timeoutId = setTimeout(() => {
2870
+ reject(_Ryoiki.ERR_TIMEOUT(id, timeout));
2871
+ }, timeout);
2872
+ }
2873
+ const id = this._createRandomId();
2874
+ const alloc = async () => {
2875
+ if (timeoutId !== null) {
2876
+ clearTimeout(timeoutId);
2877
+ }
2878
+ const [err, v] = await _Ryoiki.CatchError(task(id));
2879
+ if (err) reject(err);
2880
+ else resolve(v);
2881
+ };
2882
+ const fetch = () => {
2883
+ this.fetchUnitAndRun(this.readQueue, this.readings);
2884
+ this.fetchUnitAndRun(this.writeQueue, this.writings);
2885
+ };
2886
+ queue.set(id, { id, range, condition, alloc, free: fetch });
2887
+ fetch();
2888
+ });
2889
+ }
2890
+ _checkWorking(range, workspaces) {
2891
+ let isLocked = false;
2892
+ for (const lock of workspaces.values()) {
2893
+ if (_Ryoiki.IsRangeOverlap(range, lock.range)) {
2894
+ isLocked = true;
2895
+ break;
2896
+ }
2897
+ }
2898
+ return isLocked;
2899
+ }
2900
+ /**
2901
+ * Checks if there is any active read lock within the specified range.
2902
+ * @param range The range to check for active read locks.
2903
+ * @returns `true` if there is an active read lock within the range, `false` otherwise.
2904
+ */
2905
+ isReading(range) {
2906
+ return this._checkWorking(range, this.readings);
2907
+ }
2908
+ /**
2909
+ * Checks if there is any active write lock within the specified range.
2910
+ * @param range The range to check for active write locks.
2911
+ * @returns `true` if there is an active write lock within the range, `false` otherwise.
2912
+ */
2913
+ isWriting(range) {
2914
+ return this._checkWorking(range, this.writings);
2915
+ }
2916
+ /**
2917
+ * Checks if a read lock can be acquired within the specified range.
2918
+ * @param range The range to check for read lock availability.
2919
+ * @returns `true` if a read lock can be acquired, `false` otherwise.
2920
+ */
2921
+ canRead(range) {
2922
+ const writing = this.isWriting(range);
2923
+ return !writing;
2924
+ }
2925
+ /**
2926
+ * Checks if a write lock can be acquired within the specified range.
2927
+ * @param range The range to check for write lock availability.
2928
+ * @returns `true` if a write lock can be acquired, `false` otherwise.
2929
+ */
2930
+ canWrite(range) {
2931
+ const reading = this.isReading(range);
2932
+ const writing = this.isWriting(range);
2933
+ return !reading && !writing;
2934
+ }
2935
+ /**
2936
+ * Internal implementation of the read lock. Handles both overloads.
2937
+ * @template T - The return type of the task.
2938
+ * @param arg0 - Either a range or a task callback.
2939
+ * If a range is provided, the task is the second argument.
2940
+ * @param arg1 - The task to execute, required if a range is provided.
2941
+ * @param arg2 - The timeout for acquiring the lock.
2942
+ * If the lock cannot be acquired within this period, an error will be thrown.
2943
+ * If this value is not provided, no timeout will be set.
2944
+ * @returns A promise resolving to the result of the task execution.
2945
+ */
2946
+ readLock(arg0, arg1, arg2) {
2947
+ const [range, task, timeout] = this._handleOverload(
2948
+ [arg0, arg1, arg2],
2949
+ {
2950
+ rangeTask: (range2, task2) => [range2, task2, -1],
2951
+ rangeTaskTimeout: (range2, task2, timeout2) => [range2, task2, timeout2],
2952
+ task: (task2) => [[-Infinity, Infinity], task2, -1],
2953
+ taskTimeout: (task2, timeout2) => [[-Infinity, Infinity], task2, timeout2]
2954
+ },
2955
+ {
2956
+ task: [Function],
2957
+ taskTimeout: [Function, Number],
2958
+ rangeTask: [Array, Function],
2959
+ rangeTaskTimeout: [Array, Function, Number]
2960
+ }
2961
+ );
2962
+ return this._lock(
2963
+ this.readQueue,
2964
+ range,
2965
+ timeout,
2966
+ task,
2967
+ () => !this.rangeOverlapping(this.writings, range)
2968
+ );
2969
+ }
2970
+ /**
2971
+ * Internal implementation of the write lock. Handles both overloads.
2972
+ * @template T - The return type of the task.
2973
+ * @param arg0 - Either a range or a task callback.
2974
+ * If a range is provided, the task is the second argument.
2975
+ * @param arg1 - The task to execute, required if a range is provided.
2976
+ * @param arg2 - The timeout for acquiring the lock.
2977
+ * If the lock cannot be acquired within this period, an error will be thrown.
2978
+ * If this value is not provided, no timeout will be set.
2979
+ * @returns A promise resolving to the result of the task execution.
2980
+ */
2981
+ writeLock(arg0, arg1, arg2) {
2982
+ const [range, task, timeout] = this._handleOverload(
2983
+ [arg0, arg1, arg2],
2984
+ {
2985
+ rangeTask: (range2, task2) => [range2, task2, -1],
2986
+ rangeTaskTimeout: (range2, task2, timeout2) => [range2, task2, timeout2],
2987
+ task: (task2) => [[-Infinity, Infinity], task2, -1],
2988
+ taskTimeout: (task2, timeout2) => [[-Infinity, Infinity], task2, timeout2]
2989
+ },
2990
+ {
2991
+ task: [Function],
2992
+ taskTimeout: [Function, Number],
2993
+ rangeTask: [Array, Function],
2994
+ rangeTaskTimeout: [Array, Function, Number]
2995
+ }
2996
+ );
2997
+ return this._lock(
2998
+ this.writeQueue,
2999
+ range,
3000
+ timeout,
3001
+ task,
3002
+ () => {
3003
+ return !this.rangeOverlapping(this.writings, range) && !this.rangeOverlapping(this.readings, range);
3004
+ }
3005
+ );
3006
+ }
3007
+ /**
3008
+ * Releases a read lock by its lock ID.
3009
+ * @param lockId - The unique identifier for the lock to release.
3010
+ */
3011
+ readUnlock(lockId) {
3012
+ this._free(this.readings, lockId);
3013
+ }
3014
+ /**
3015
+ * Releases a write lock by its lock ID.
3016
+ * @param lockId - The unique identifier for the lock to release.
3017
+ */
3018
+ writeUnlock(lockId) {
3019
+ this._free(this.writings, lockId);
3020
+ }
3021
+ };
3022
+
2679
3023
  // src/SerializeStrategyAsync.ts
2680
3024
  var SerializeStrategyAsync = class extends SerializeStrategy {
3025
+ lock = new Ryoiki();
3026
+ async acquireLock(action) {
3027
+ let lockId;
3028
+ return this.lock.writeLock((_lockId) => {
3029
+ lockId = _lockId;
3030
+ return action();
3031
+ }).finally(() => {
3032
+ this.lock.writeUnlock(lockId);
3033
+ });
3034
+ }
2681
3035
  async getHeadData(key, defaultValue) {
2682
3036
  if (!Object.hasOwn(this.head.data, key)) {
2683
3037
  await this.setHeadData(key, defaultValue);
@@ -2694,13 +3048,13 @@ var SerializeStrategyAsync = class extends SerializeStrategy {
2694
3048
  await this.setHeadData(key, next);
2695
3049
  return current;
2696
3050
  }
2697
- async compareAndSwapHead(oldRoot, newRoot) {
2698
- if (this.head.root !== oldRoot) {
2699
- return false;
2700
- }
3051
+ async getLastCommittedTransactionId() {
3052
+ return this.lastCommittedTransactionId;
3053
+ }
3054
+ async compareAndSwapHead(newRoot, newTxId) {
2701
3055
  this.head.root = newRoot;
3056
+ this.lastCommittedTransactionId = newTxId;
2702
3057
  await this.writeHead(this.head);
2703
- return true;
2704
3058
  }
2705
3059
  };
2706
3060
  var InMemoryStoreStrategyAsync = class extends SerializeStrategyAsync {
@@ -2769,8 +3123,11 @@ var BPTreeAsyncSnapshotStrategy = class extends SerializeStrategyAsync {
2769
3123
  this.snapshotHead.root = head.root;
2770
3124
  this.snapshotHead.data = { ...head.data };
2771
3125
  }
2772
- async compareAndSwapHead(oldRoot, newRoot) {
2773
- return await this.baseStrategy.compareAndSwapHead(oldRoot, newRoot);
3126
+ async compareAndSwapHead(newRoot, newTxId) {
3127
+ await this.baseStrategy.compareAndSwapHead(newRoot, newTxId);
3128
+ }
3129
+ async getLastCommittedTransactionId() {
3130
+ return await this.baseStrategy.getLastCommittedTransactionId();
2774
3131
  }
2775
3132
  async getHeadData(key, defaultValue) {
2776
3133
  return this.snapshotHead.data[key] ?? defaultValue;
@@ -2791,8 +3148,11 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2791
3148
  dirtyIds;
2792
3149
  createdInTx;
2793
3150
  deletedIds;
3151
+ originalNodes = /* @__PURE__ */ new Map();
2794
3152
  initialRootId;
2795
3153
  transactionRootId;
3154
+ transactionId;
3155
+ initialLastCommittedTransactionId = 0;
2796
3156
  constructor(baseTree) {
2797
3157
  super(baseTree.strategy, baseTree.comparator, baseTree.option);
2798
3158
  this.realBaseTree = baseTree;
@@ -2803,6 +3163,7 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2803
3163
  this.dirtyIds = /* @__PURE__ */ new Set();
2804
3164
  this.createdInTx = /* @__PURE__ */ new Set();
2805
3165
  this.deletedIds = /* @__PURE__ */ new Set();
3166
+ this.transactionId = Date.now() + Math.random();
2806
3167
  }
2807
3168
  /**
2808
3169
  * Initializes the transaction by capturing the current state of the tree.
@@ -2819,6 +3180,7 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2819
3180
  const root = await this._createNode(true, [], [], true);
2820
3181
  this.initialRootId = root.id;
2821
3182
  }
3183
+ this.initialLastCommittedTransactionId = await this.realBaseStrategy.getLastCommittedTransactionId();
2822
3184
  this.transactionRootId = this.initialRootId;
2823
3185
  this.rootId = this.transactionRootId;
2824
3186
  const snapshotStrategy = new BPTreeAsyncSnapshotStrategy(this.realBaseStrategy, this.initialRootId);
@@ -2827,6 +3189,7 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2827
3189
  this.dirtyIds.clear();
2828
3190
  this.createdInTx.clear();
2829
3191
  this.deletedIds.clear();
3192
+ this.realBaseTree.registerTransaction(this.transactionId);
2830
3193
  }
2831
3194
  async getNode(id) {
2832
3195
  if (this.txNodes.has(id)) {
@@ -2835,7 +3198,13 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2835
3198
  if (this.deletedIds.has(id)) {
2836
3199
  throw new Error(`The tree attempted to reference deleted node '${id}'`);
2837
3200
  }
2838
- const baseNode = await this.realBaseStrategy.read(id);
3201
+ let baseNode = this.realBaseTree.getObsoleteNode(id);
3202
+ if (!baseNode) {
3203
+ baseNode = await this.realBaseStrategy.read(id);
3204
+ }
3205
+ if (!this.originalNodes.has(id) && !this.createdInTx.has(id)) {
3206
+ this.originalNodes.set(id, JSON.parse(JSON.stringify(baseNode)));
3207
+ }
2839
3208
  const clone = JSON.parse(JSON.stringify(baseNode));
2840
3209
  this.txNodes.set(id, clone);
2841
3210
  return clone;
@@ -2925,9 +3294,10 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2925
3294
  * Attempts to commit the transaction.
2926
3295
  * Uses Optimistic Locking (Compare-And-Swap) on the root node ID to detect conflicts.
2927
3296
  *
3297
+ * @param cleanup Whether to clean up obsolete nodes after commit. Defaults to true.
2928
3298
  * @returns A promise that resolves to the transaction result.
2929
3299
  */
2930
- async commit() {
3300
+ async commit(cleanup = true) {
2931
3301
  const idMapping = /* @__PURE__ */ new Map();
2932
3302
  const finalNodes = [];
2933
3303
  for (const oldId of this.dirtyIds) {
@@ -2975,24 +3345,51 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2975
3345
  if (idMapping.has(this.rootId)) {
2976
3346
  newRootId = idMapping.get(this.rootId);
2977
3347
  }
2978
- for (const node of finalNodes) {
2979
- await this.realBaseStrategy.write(node.id, node);
3348
+ let success = false;
3349
+ if (finalNodes.length === 0) {
3350
+ success = true;
3351
+ } else {
3352
+ success = await this.realBaseStrategy.acquireLock(async () => {
3353
+ if (await this.realBaseStrategy.getLastCommittedTransactionId() === this.initialLastCommittedTransactionId) {
3354
+ for (const node of finalNodes) {
3355
+ await this.realBaseStrategy.write(node.id, node);
3356
+ }
3357
+ await this.realBaseStrategy.compareAndSwapHead(newRootId, this.transactionId);
3358
+ return true;
3359
+ }
3360
+ return false;
3361
+ });
2980
3362
  }
2981
- const success = await this.realBaseStrategy.compareAndSwapHead(this.initialRootId, newRootId);
2982
3363
  if (success) {
2983
3364
  const distinctObsolete = /* @__PURE__ */ new Set();
2984
3365
  for (const oldId of this.dirtyIds) {
2985
- if (!this.createdInTx.has(oldId) && this.txNodes.has(oldId)) {
3366
+ if (this.createdInTx.has(oldId)) {
3367
+ continue;
3368
+ }
3369
+ if (this.txNodes.has(oldId) || this.deletedIds.has(oldId)) {
2986
3370
  distinctObsolete.add(oldId);
2987
3371
  }
2988
3372
  }
3373
+ if (cleanup) {
3374
+ for (const obsoleteId of distinctObsolete) {
3375
+ if (this.originalNodes.has(obsoleteId)) {
3376
+ this.realBaseTree.addObsoleteNode(
3377
+ this.originalNodes.get(obsoleteId),
3378
+ this.transactionId
3379
+ );
3380
+ }
3381
+ await this.realBaseStrategy.delete(obsoleteId);
3382
+ }
3383
+ }
3384
+ this.realBaseTree.unregisterTransaction(this.transactionId);
3385
+ this.realBaseTree.pruneObsoleteNodes();
2989
3386
  return {
2990
3387
  success: true,
2991
3388
  createdIds: newCreatedIds,
2992
3389
  obsoleteIds: Array.from(distinctObsolete)
2993
3390
  };
2994
3391
  } else {
2995
- await this.rollback();
3392
+ await this.rollback(cleanup);
2996
3393
  return {
2997
3394
  success: false,
2998
3395
  createdIds: newCreatedIds,
@@ -3016,6 +3413,8 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
3016
3413
  await this.realBaseStrategy.delete(id);
3017
3414
  }
3018
3415
  }
3416
+ this.realBaseTree.unregisterTransaction(this.transactionId);
3417
+ this.realBaseTree.pruneObsoleteNodes();
3019
3418
  return createdIds;
3020
3419
  }
3021
3420
  async readLock(fn) {