serializable-bptree 7.0.1 → 7.0.3

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,12 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1647
1688
  dirtyIds;
1648
1689
  createdInTx;
1649
1690
  deletedIds;
1691
+ obsoleteNodes = /* @__PURE__ */ new Map();
1692
+ originalNodes = /* @__PURE__ */ new Map();
1650
1693
  initialRootId;
1651
1694
  transactionRootId;
1695
+ transactionId;
1696
+ initialLastCommittedTransactionId = 0;
1652
1697
  constructor(baseTree) {
1653
1698
  super(baseTree.strategy, baseTree.comparator, baseTree.option);
1654
1699
  this.realBaseTree = baseTree;
@@ -1659,6 +1704,7 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1659
1704
  this.dirtyIds = /* @__PURE__ */ new Set();
1660
1705
  this.createdInTx = /* @__PURE__ */ new Set();
1661
1706
  this.deletedIds = /* @__PURE__ */ new Set();
1707
+ this.transactionId = Date.now() + Math.random();
1662
1708
  }
1663
1709
  /**
1664
1710
  * Initializes the transaction by capturing the current state of the tree.
@@ -1675,6 +1721,7 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1675
1721
  const root = this._createNode(true, [], [], true);
1676
1722
  this.initialRootId = root.id;
1677
1723
  }
1724
+ this.initialLastCommittedTransactionId = this.realBaseStrategy.getLastCommittedTransactionId();
1678
1725
  this.transactionRootId = this.initialRootId;
1679
1726
  this.rootId = this.transactionRootId;
1680
1727
  const snapshotStrategy = new BPTreeSyncSnapshotStrategy(this.realBaseStrategy, this.initialRootId);
@@ -1683,6 +1730,7 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1683
1730
  this.dirtyIds.clear();
1684
1731
  this.createdInTx.clear();
1685
1732
  this.deletedIds.clear();
1733
+ this.realBaseTree.registerTransaction(this.transactionId);
1686
1734
  }
1687
1735
  getNode(id) {
1688
1736
  if (this.txNodes.has(id)) {
@@ -1691,7 +1739,13 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1691
1739
  if (this.deletedIds.has(id)) {
1692
1740
  throw new Error(`The tree attempted to reference deleted node '${id}'`);
1693
1741
  }
1694
- const baseNode = this.realBaseStrategy.read(id);
1742
+ let baseNode = this.realBaseTree.getObsoleteNode(id);
1743
+ if (!baseNode) {
1744
+ baseNode = this.realBaseStrategy.read(id);
1745
+ }
1746
+ if (!this.originalNodes.has(id) && !this.createdInTx.has(id)) {
1747
+ this.originalNodes.set(id, JSON.parse(JSON.stringify(baseNode)));
1748
+ }
1695
1749
  const clone = JSON.parse(JSON.stringify(baseNode));
1696
1750
  this.txNodes.set(id, clone);
1697
1751
  return clone;
@@ -1781,9 +1835,10 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1781
1835
  * Attempts to commit the transaction.
1782
1836
  * Uses Optimistic Locking (Compare-And-Swap) on the root node ID to detect conflicts.
1783
1837
  *
1838
+ * @param cleanup Whether to clean up obsolete nodes after commit. Defaults to true.
1784
1839
  * @returns The transaction result.
1785
1840
  */
1786
- commit() {
1841
+ commit(cleanup = true) {
1787
1842
  const idMapping = /* @__PURE__ */ new Map();
1788
1843
  const finalNodes = [];
1789
1844
  for (const oldId of this.dirtyIds) {
@@ -1831,24 +1886,47 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1831
1886
  if (idMapping.has(this.rootId)) {
1832
1887
  newRootId = idMapping.get(this.rootId);
1833
1888
  }
1834
- for (const node of finalNodes) {
1835
- this.realBaseStrategy.write(node.id, node);
1889
+ let success = false;
1890
+ if (finalNodes.length === 0) {
1891
+ success = true;
1892
+ } else if (this.realBaseStrategy.getLastCommittedTransactionId() === this.initialLastCommittedTransactionId) {
1893
+ for (const node of finalNodes) {
1894
+ this.realBaseStrategy.write(node.id, node);
1895
+ }
1896
+ this.realBaseStrategy.compareAndSwapHead(newRootId, this.transactionId);
1897
+ success = true;
1836
1898
  }
1837
- const success = this.realBaseStrategy.compareAndSwapHead(this.initialRootId, newRootId);
1838
1899
  if (success) {
1839
1900
  const distinctObsolete = /* @__PURE__ */ new Set();
1840
1901
  for (const oldId of this.dirtyIds) {
1841
- if (!this.createdInTx.has(oldId) && this.txNodes.has(oldId)) {
1842
- distinctObsolete.add(oldId);
1902
+ if (!this.createdInTx.has(oldId)) {
1903
+ if (this.txNodes.has(oldId) || this.deletedIds.has(oldId)) {
1904
+ distinctObsolete.add(oldId);
1905
+ if (this.originalNodes.has(oldId)) {
1906
+ this.obsoleteNodes.set(oldId, this.originalNodes.get(oldId));
1907
+ }
1908
+ }
1843
1909
  }
1844
1910
  }
1911
+ if (cleanup) {
1912
+ for (const obsoleteId of distinctObsolete) {
1913
+ if (this.originalNodes.has(obsoleteId)) {
1914
+ this.realBaseTree.addObsoleteNode(
1915
+ this.originalNodes.get(obsoleteId),
1916
+ this.transactionId
1917
+ );
1918
+ }
1919
+ this.realBaseStrategy.delete(obsoleteId);
1920
+ }
1921
+ }
1922
+ this.realBaseTree.unregisterTransaction(this.transactionId);
1845
1923
  return {
1846
1924
  success: true,
1847
1925
  createdIds: newCreatedIds,
1848
1926
  obsoleteIds: Array.from(distinctObsolete)
1849
1927
  };
1850
1928
  } else {
1851
- this.rollback();
1929
+ this.rollback(cleanup);
1852
1930
  return {
1853
1931
  success: false,
1854
1932
  createdIds: newCreatedIds,
@@ -1858,12 +1936,28 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1858
1936
  }
1859
1937
  /**
1860
1938
  * Rolls back the transaction by clearing all buffered changes.
1861
- * Internal use only.
1939
+ * If cleanup is `true`, it also clears the transaction nodes.
1940
+ * @param cleanup Whether to clear the transaction nodes.
1941
+ * @returns The IDs of nodes that were created in this transaction.
1862
1942
  */
1863
- rollback() {
1943
+ rollback(cleanup = true) {
1944
+ const createdIds = Array.from(this.createdInTx);
1864
1945
  this.txNodes.clear();
1865
1946
  this.dirtyIds.clear();
1866
1947
  this.createdInTx.clear();
1948
+ if (cleanup) {
1949
+ for (const id of createdIds) {
1950
+ this.realBaseStrategy.delete(id);
1951
+ }
1952
+ }
1953
+ this.realBaseTree.unregisterTransaction(this.transactionId);
1954
+ return createdIds;
1955
+ }
1956
+ readLock(fn) {
1957
+ return fn();
1958
+ }
1959
+ writeLock(fn) {
1960
+ return fn();
1867
1961
  }
1868
1962
  // Override to do nothing, as transaction handles its own commits
1869
1963
  commitHeadBuffer() {
@@ -2502,7 +2596,9 @@ var BPTreeAsyncBase = class extends BPTree {
2502
2596
  this._nodeUpdateBuffer.clear();
2503
2597
  }
2504
2598
  async commitNodeDeleteBuffer() {
2599
+ const obsoleteAt = this.getNextTransactionId();
2505
2600
  for (const node of this._nodeDeleteBuffer.values()) {
2601
+ this.addObsoleteNode(node, obsoleteAt);
2506
2602
  await this.strategy.delete(node.id);
2507
2603
  this.nodes.delete(node.id);
2508
2604
  }
@@ -2667,8 +2763,276 @@ var BPTreeAsyncBase = class extends BPTree {
2667
2763
  }
2668
2764
  };
2669
2765
 
2766
+ // node_modules/ryoiki/dist/esm/index.mjs
2767
+ var Ryoiki = class _Ryoiki {
2768
+ readings;
2769
+ writings;
2770
+ readQueue;
2771
+ writeQueue;
2772
+ static async CatchError(promise) {
2773
+ return await promise.then((v) => [void 0, v]).catch((err) => [err]);
2774
+ }
2775
+ static IsRangeOverlap(a, b) {
2776
+ const [start1, end1] = a;
2777
+ const [start2, end2] = b;
2778
+ if (end1 <= start2 || end2 <= start1) {
2779
+ return false;
2780
+ }
2781
+ return true;
2782
+ }
2783
+ static ERR_ALREADY_EXISTS(lockId) {
2784
+ return new Error(`The '${lockId}' task already existing in queue or running.`);
2785
+ }
2786
+ static ERR_NOT_EXISTS(lockId) {
2787
+ return new Error(`The '${lockId}' task not existing in task queue.`);
2788
+ }
2789
+ static ERR_TIMEOUT(lockId, timeout) {
2790
+ return new Error(`The task with ID '${lockId}' failed to acquire the lock within the timeout(${timeout}ms).`);
2791
+ }
2792
+ /**
2793
+ * Constructs a new instance of the Ryoiki class.
2794
+ */
2795
+ constructor() {
2796
+ this.readings = /* @__PURE__ */ new Map();
2797
+ this.writings = /* @__PURE__ */ new Map();
2798
+ this.readQueue = /* @__PURE__ */ new Map();
2799
+ this.writeQueue = /* @__PURE__ */ new Map();
2800
+ }
2801
+ /**
2802
+ * Creates a range based on a start value and length.
2803
+ * @param start - The starting value of the range.
2804
+ * @param length - The length of the range.
2805
+ * @returns A range tuple [start, start + length].
2806
+ */
2807
+ range(start, length) {
2808
+ return [start, start + length];
2809
+ }
2810
+ rangeOverlapping(tasks, range) {
2811
+ return Array.from(tasks.values()).some((t) => _Ryoiki.IsRangeOverlap(t.range, range));
2812
+ }
2813
+ isSameRange(a, b) {
2814
+ const [a1, a2] = a;
2815
+ const [b1, b2] = b;
2816
+ return a1 === b1 && a2 === b2;
2817
+ }
2818
+ fetchUnitAndRun(queue, workspaces) {
2819
+ for (const [id, unit] of queue) {
2820
+ if (!unit.condition()) {
2821
+ continue;
2822
+ }
2823
+ this._alloc(queue, workspaces, id);
2824
+ }
2825
+ }
2826
+ _handleOverload(args, handlers, argPatterns) {
2827
+ for (const [key, pattern] of Object.entries(argPatterns)) {
2828
+ if (this._matchArgs(args, pattern)) {
2829
+ return handlers[key](...args);
2830
+ }
2831
+ }
2832
+ throw new Error("Invalid arguments");
2833
+ }
2834
+ _matchArgs(args, pattern) {
2835
+ return args.every((arg, index) => {
2836
+ const expectedType = pattern[index];
2837
+ if (expectedType === void 0) return typeof arg === "undefined";
2838
+ if (expectedType === Function) return typeof arg === "function";
2839
+ if (expectedType === Number) return typeof arg === "number";
2840
+ if (expectedType === Array) return Array.isArray(arg);
2841
+ return false;
2842
+ });
2843
+ }
2844
+ _createRandomId() {
2845
+ const timestamp = Date.now().toString(36);
2846
+ const random = Math.random().toString(36).substring(2);
2847
+ return `${timestamp}${random}`;
2848
+ }
2849
+ _alloc(queue, workspaces, lockId) {
2850
+ const unit = queue.get(lockId);
2851
+ if (!unit) {
2852
+ throw _Ryoiki.ERR_NOT_EXISTS(lockId);
2853
+ }
2854
+ workspaces.set(lockId, unit);
2855
+ queue.delete(lockId);
2856
+ unit.alloc();
2857
+ }
2858
+ _free(workspaces, lockId) {
2859
+ const unit = workspaces.get(lockId);
2860
+ if (!unit) {
2861
+ throw _Ryoiki.ERR_NOT_EXISTS(lockId);
2862
+ }
2863
+ workspaces.delete(lockId);
2864
+ unit.free();
2865
+ }
2866
+ _lock(queue, range, timeout, task, condition) {
2867
+ return new Promise((resolve, reject) => {
2868
+ let timeoutId = null;
2869
+ if (timeout >= 0) {
2870
+ timeoutId = setTimeout(() => {
2871
+ reject(_Ryoiki.ERR_TIMEOUT(id, timeout));
2872
+ }, timeout);
2873
+ }
2874
+ const id = this._createRandomId();
2875
+ const alloc = async () => {
2876
+ if (timeoutId !== null) {
2877
+ clearTimeout(timeoutId);
2878
+ }
2879
+ const [err, v] = await _Ryoiki.CatchError(task(id));
2880
+ if (err) reject(err);
2881
+ else resolve(v);
2882
+ };
2883
+ const fetch = () => {
2884
+ this.fetchUnitAndRun(this.readQueue, this.readings);
2885
+ this.fetchUnitAndRun(this.writeQueue, this.writings);
2886
+ };
2887
+ queue.set(id, { id, range, condition, alloc, free: fetch });
2888
+ fetch();
2889
+ });
2890
+ }
2891
+ _checkWorking(range, workspaces) {
2892
+ let isLocked = false;
2893
+ for (const lock of workspaces.values()) {
2894
+ if (_Ryoiki.IsRangeOverlap(range, lock.range)) {
2895
+ isLocked = true;
2896
+ break;
2897
+ }
2898
+ }
2899
+ return isLocked;
2900
+ }
2901
+ /**
2902
+ * Checks if there is any active read lock within the specified range.
2903
+ * @param range The range to check for active read locks.
2904
+ * @returns `true` if there is an active read lock within the range, `false` otherwise.
2905
+ */
2906
+ isReading(range) {
2907
+ return this._checkWorking(range, this.readings);
2908
+ }
2909
+ /**
2910
+ * Checks if there is any active write lock within the specified range.
2911
+ * @param range The range to check for active write locks.
2912
+ * @returns `true` if there is an active write lock within the range, `false` otherwise.
2913
+ */
2914
+ isWriting(range) {
2915
+ return this._checkWorking(range, this.writings);
2916
+ }
2917
+ /**
2918
+ * Checks if a read lock can be acquired within the specified range.
2919
+ * @param range The range to check for read lock availability.
2920
+ * @returns `true` if a read lock can be acquired, `false` otherwise.
2921
+ */
2922
+ canRead(range) {
2923
+ const writing = this.isWriting(range);
2924
+ return !writing;
2925
+ }
2926
+ /**
2927
+ * Checks if a write lock can be acquired within the specified range.
2928
+ * @param range The range to check for write lock availability.
2929
+ * @returns `true` if a write lock can be acquired, `false` otherwise.
2930
+ */
2931
+ canWrite(range) {
2932
+ const reading = this.isReading(range);
2933
+ const writing = this.isWriting(range);
2934
+ return !reading && !writing;
2935
+ }
2936
+ /**
2937
+ * Internal implementation of the read lock. Handles both overloads.
2938
+ * @template T - The return type of the task.
2939
+ * @param arg0 - Either a range or a task callback.
2940
+ * If a range is provided, the task is the second argument.
2941
+ * @param arg1 - The task to execute, required if a range is provided.
2942
+ * @param arg2 - The timeout for acquiring the lock.
2943
+ * If the lock cannot be acquired within this period, an error will be thrown.
2944
+ * If this value is not provided, no timeout will be set.
2945
+ * @returns A promise resolving to the result of the task execution.
2946
+ */
2947
+ readLock(arg0, arg1, arg2) {
2948
+ const [range, task, timeout] = this._handleOverload(
2949
+ [arg0, arg1, arg2],
2950
+ {
2951
+ rangeTask: (range2, task2) => [range2, task2, -1],
2952
+ rangeTaskTimeout: (range2, task2, timeout2) => [range2, task2, timeout2],
2953
+ task: (task2) => [[-Infinity, Infinity], task2, -1],
2954
+ taskTimeout: (task2, timeout2) => [[-Infinity, Infinity], task2, timeout2]
2955
+ },
2956
+ {
2957
+ task: [Function],
2958
+ taskTimeout: [Function, Number],
2959
+ rangeTask: [Array, Function],
2960
+ rangeTaskTimeout: [Array, Function, Number]
2961
+ }
2962
+ );
2963
+ return this._lock(
2964
+ this.readQueue,
2965
+ range,
2966
+ timeout,
2967
+ task,
2968
+ () => !this.rangeOverlapping(this.writings, range)
2969
+ );
2970
+ }
2971
+ /**
2972
+ * Internal implementation of the write lock. Handles both overloads.
2973
+ * @template T - The return type of the task.
2974
+ * @param arg0 - Either a range or a task callback.
2975
+ * If a range is provided, the task is the second argument.
2976
+ * @param arg1 - The task to execute, required if a range is provided.
2977
+ * @param arg2 - The timeout for acquiring the lock.
2978
+ * If the lock cannot be acquired within this period, an error will be thrown.
2979
+ * If this value is not provided, no timeout will be set.
2980
+ * @returns A promise resolving to the result of the task execution.
2981
+ */
2982
+ writeLock(arg0, arg1, arg2) {
2983
+ const [range, task, timeout] = this._handleOverload(
2984
+ [arg0, arg1, arg2],
2985
+ {
2986
+ rangeTask: (range2, task2) => [range2, task2, -1],
2987
+ rangeTaskTimeout: (range2, task2, timeout2) => [range2, task2, timeout2],
2988
+ task: (task2) => [[-Infinity, Infinity], task2, -1],
2989
+ taskTimeout: (task2, timeout2) => [[-Infinity, Infinity], task2, timeout2]
2990
+ },
2991
+ {
2992
+ task: [Function],
2993
+ taskTimeout: [Function, Number],
2994
+ rangeTask: [Array, Function],
2995
+ rangeTaskTimeout: [Array, Function, Number]
2996
+ }
2997
+ );
2998
+ return this._lock(
2999
+ this.writeQueue,
3000
+ range,
3001
+ timeout,
3002
+ task,
3003
+ () => {
3004
+ return !this.rangeOverlapping(this.writings, range) && !this.rangeOverlapping(this.readings, range);
3005
+ }
3006
+ );
3007
+ }
3008
+ /**
3009
+ * Releases a read lock by its lock ID.
3010
+ * @param lockId - The unique identifier for the lock to release.
3011
+ */
3012
+ readUnlock(lockId) {
3013
+ this._free(this.readings, lockId);
3014
+ }
3015
+ /**
3016
+ * Releases a write lock by its lock ID.
3017
+ * @param lockId - The unique identifier for the lock to release.
3018
+ */
3019
+ writeUnlock(lockId) {
3020
+ this._free(this.writings, lockId);
3021
+ }
3022
+ };
3023
+
2670
3024
  // src/SerializeStrategyAsync.ts
2671
3025
  var SerializeStrategyAsync = class extends SerializeStrategy {
3026
+ lock = new Ryoiki();
3027
+ async acquireLock(action) {
3028
+ let lockId;
3029
+ return this.lock.writeLock((_lockId) => {
3030
+ lockId = _lockId;
3031
+ return action();
3032
+ }).finally(() => {
3033
+ this.lock.writeUnlock(lockId);
3034
+ });
3035
+ }
2672
3036
  async getHeadData(key, defaultValue) {
2673
3037
  if (!Object.hasOwn(this.head.data, key)) {
2674
3038
  await this.setHeadData(key, defaultValue);
@@ -2685,13 +3049,13 @@ var SerializeStrategyAsync = class extends SerializeStrategy {
2685
3049
  await this.setHeadData(key, next);
2686
3050
  return current;
2687
3051
  }
2688
- async compareAndSwapHead(oldRoot, newRoot) {
2689
- if (this.head.root !== oldRoot) {
2690
- return false;
2691
- }
3052
+ async getLastCommittedTransactionId() {
3053
+ return this.lastCommittedTransactionId;
3054
+ }
3055
+ async compareAndSwapHead(newRoot, newTxId) {
2692
3056
  this.head.root = newRoot;
3057
+ this.lastCommittedTransactionId = newTxId;
2693
3058
  await this.writeHead(this.head);
2694
- return true;
2695
3059
  }
2696
3060
  };
2697
3061
  var InMemoryStoreStrategyAsync = class extends SerializeStrategyAsync {
@@ -2760,8 +3124,11 @@ var BPTreeAsyncSnapshotStrategy = class extends SerializeStrategyAsync {
2760
3124
  this.snapshotHead.root = head.root;
2761
3125
  this.snapshotHead.data = { ...head.data };
2762
3126
  }
2763
- async compareAndSwapHead(oldRoot, newRoot) {
2764
- return await this.baseStrategy.compareAndSwapHead(oldRoot, newRoot);
3127
+ async compareAndSwapHead(newRoot, newTxId) {
3128
+ await this.baseStrategy.compareAndSwapHead(newRoot, newTxId);
3129
+ }
3130
+ async getLastCommittedTransactionId() {
3131
+ return await this.baseStrategy.getLastCommittedTransactionId();
2765
3132
  }
2766
3133
  async getHeadData(key, defaultValue) {
2767
3134
  return this.snapshotHead.data[key] ?? defaultValue;
@@ -2782,8 +3149,12 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2782
3149
  dirtyIds;
2783
3150
  createdInTx;
2784
3151
  deletedIds;
3152
+ obsoleteNodes = /* @__PURE__ */ new Map();
3153
+ originalNodes = /* @__PURE__ */ new Map();
2785
3154
  initialRootId;
2786
3155
  transactionRootId;
3156
+ transactionId;
3157
+ initialLastCommittedTransactionId = 0;
2787
3158
  constructor(baseTree) {
2788
3159
  super(baseTree.strategy, baseTree.comparator, baseTree.option);
2789
3160
  this.realBaseTree = baseTree;
@@ -2794,6 +3165,7 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2794
3165
  this.dirtyIds = /* @__PURE__ */ new Set();
2795
3166
  this.createdInTx = /* @__PURE__ */ new Set();
2796
3167
  this.deletedIds = /* @__PURE__ */ new Set();
3168
+ this.transactionId = Date.now() + Math.random();
2797
3169
  }
2798
3170
  /**
2799
3171
  * Initializes the transaction by capturing the current state of the tree.
@@ -2810,6 +3182,7 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2810
3182
  const root = await this._createNode(true, [], [], true);
2811
3183
  this.initialRootId = root.id;
2812
3184
  }
3185
+ this.initialLastCommittedTransactionId = await this.realBaseStrategy.getLastCommittedTransactionId();
2813
3186
  this.transactionRootId = this.initialRootId;
2814
3187
  this.rootId = this.transactionRootId;
2815
3188
  const snapshotStrategy = new BPTreeAsyncSnapshotStrategy(this.realBaseStrategy, this.initialRootId);
@@ -2818,6 +3191,7 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2818
3191
  this.dirtyIds.clear();
2819
3192
  this.createdInTx.clear();
2820
3193
  this.deletedIds.clear();
3194
+ this.realBaseTree.registerTransaction(this.transactionId);
2821
3195
  }
2822
3196
  async getNode(id) {
2823
3197
  if (this.txNodes.has(id)) {
@@ -2826,7 +3200,13 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2826
3200
  if (this.deletedIds.has(id)) {
2827
3201
  throw new Error(`The tree attempted to reference deleted node '${id}'`);
2828
3202
  }
2829
- const baseNode = await this.realBaseStrategy.read(id);
3203
+ let baseNode = this.realBaseTree.getObsoleteNode(id);
3204
+ if (!baseNode) {
3205
+ baseNode = await this.realBaseStrategy.read(id);
3206
+ }
3207
+ if (!this.originalNodes.has(id) && !this.createdInTx.has(id)) {
3208
+ this.originalNodes.set(id, JSON.parse(JSON.stringify(baseNode)));
3209
+ }
2830
3210
  const clone = JSON.parse(JSON.stringify(baseNode));
2831
3211
  this.txNodes.set(id, clone);
2832
3212
  return clone;
@@ -2916,9 +3296,10 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2916
3296
  * Attempts to commit the transaction.
2917
3297
  * Uses Optimistic Locking (Compare-And-Swap) on the root node ID to detect conflicts.
2918
3298
  *
3299
+ * @param cleanup Whether to clean up obsolete nodes after commit. Defaults to true.
2919
3300
  * @returns A promise that resolves to the transaction result.
2920
3301
  */
2921
- async commit() {
3302
+ async commit(cleanup = true) {
2922
3303
  const idMapping = /* @__PURE__ */ new Map();
2923
3304
  const finalNodes = [];
2924
3305
  for (const oldId of this.dirtyIds) {
@@ -2966,24 +3347,52 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2966
3347
  if (idMapping.has(this.rootId)) {
2967
3348
  newRootId = idMapping.get(this.rootId);
2968
3349
  }
2969
- for (const node of finalNodes) {
2970
- await this.realBaseStrategy.write(node.id, node);
3350
+ let success = false;
3351
+ if (finalNodes.length === 0) {
3352
+ success = true;
3353
+ } else {
3354
+ success = await this.realBaseStrategy.acquireLock(async () => {
3355
+ if (await this.realBaseStrategy.getLastCommittedTransactionId() === this.initialLastCommittedTransactionId) {
3356
+ for (const node of finalNodes) {
3357
+ await this.realBaseStrategy.write(node.id, node);
3358
+ }
3359
+ await this.realBaseStrategy.compareAndSwapHead(newRootId, this.transactionId);
3360
+ return true;
3361
+ }
3362
+ return false;
3363
+ });
2971
3364
  }
2972
- const success = await this.realBaseStrategy.compareAndSwapHead(this.initialRootId, newRootId);
2973
3365
  if (success) {
2974
3366
  const distinctObsolete = /* @__PURE__ */ new Set();
2975
3367
  for (const oldId of this.dirtyIds) {
2976
- if (!this.createdInTx.has(oldId) && this.txNodes.has(oldId)) {
2977
- distinctObsolete.add(oldId);
3368
+ if (!this.createdInTx.has(oldId)) {
3369
+ if (this.txNodes.has(oldId) || this.deletedIds.has(oldId)) {
3370
+ distinctObsolete.add(oldId);
3371
+ if (this.originalNodes.has(oldId)) {
3372
+ this.obsoleteNodes.set(oldId, this.originalNodes.get(oldId));
3373
+ }
3374
+ }
3375
+ }
3376
+ }
3377
+ if (cleanup) {
3378
+ for (const obsoleteId of distinctObsolete) {
3379
+ if (this.originalNodes.has(obsoleteId)) {
3380
+ this.realBaseTree.addObsoleteNode(
3381
+ this.originalNodes.get(obsoleteId),
3382
+ this.transactionId
3383
+ );
3384
+ }
3385
+ await this.realBaseStrategy.delete(obsoleteId);
2978
3386
  }
2979
3387
  }
3388
+ this.realBaseTree.unregisterTransaction(this.transactionId);
2980
3389
  return {
2981
3390
  success: true,
2982
3391
  createdIds: newCreatedIds,
2983
3392
  obsoleteIds: Array.from(distinctObsolete)
2984
3393
  };
2985
3394
  } else {
2986
- await this.rollback();
3395
+ await this.rollback(cleanup);
2987
3396
  return {
2988
3397
  success: false,
2989
3398
  createdIds: newCreatedIds,
@@ -2993,12 +3402,22 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2993
3402
  }
2994
3403
  /**
2995
3404
  * Rolls back the transaction by clearing all buffered changes.
2996
- * Internal use only.
3405
+ * If cleanup is `true`, it also clears the transaction nodes.
3406
+ * @param cleanup Whether to clear the transaction nodes.
3407
+ * @returns The IDs of nodes that were created in this transaction.
2997
3408
  */
2998
- async rollback() {
3409
+ async rollback(cleanup = true) {
3410
+ const createdIds = Array.from(this.createdInTx);
2999
3411
  this.txNodes.clear();
3000
3412
  this.dirtyIds.clear();
3001
3413
  this.createdInTx.clear();
3414
+ if (cleanup) {
3415
+ for (const id of createdIds) {
3416
+ await this.realBaseStrategy.delete(id);
3417
+ }
3418
+ }
3419
+ this.realBaseTree.unregisterTransaction(this.transactionId);
3420
+ return createdIds;
3002
3421
  }
3003
3422
  async readLock(fn) {
3004
3423
  return await fn();