serializable-bptree 7.0.2 → 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.
@@ -439,24 +439,13 @@ var BPTree = class _BPTree {
439
439
  option;
440
440
  order;
441
441
  rootId;
442
- /**
443
- * Returns the ID of the root node.
444
- * @returns The root node ID.
445
- */
446
- getRootId() {
447
- return this.rootId;
448
- }
449
- /**
450
- * Returns the order of the B+Tree.
451
- * @returns The order of the tree.
452
- */
453
- getOrder() {
454
- return this.order;
455
- }
456
442
  _strategyDirty;
457
443
  _nodeCreateBuffer;
458
444
  _nodeUpdateBuffer;
459
445
  _nodeDeleteBuffer;
446
+ sharedDeleteCache = /* @__PURE__ */ new Map();
447
+ activeTransactions = /* @__PURE__ */ new Set();
448
+ lastTransactionId = 0;
460
449
  verifierMap = {
461
450
  gt: (nv, v) => this.comparator.isHigher(nv, v),
462
451
  gte: (nv, v) => this.comparator.isHigher(nv, v) || this.comparator.isSame(nv, v),
@@ -617,6 +606,20 @@ var BPTree = class _BPTree {
617
606
  }
618
607
  return best;
619
608
  }
609
+ /**
610
+ * Returns the ID of the root node.
611
+ * @returns The root node ID.
612
+ */
613
+ getRootId() {
614
+ return this.rootId;
615
+ }
616
+ /**
617
+ * Returns the order of the B+Tree.
618
+ * @returns The order of the tree.
619
+ */
620
+ getOrder() {
621
+ return this.order;
622
+ }
620
623
  /**
621
624
  * Verified if the value satisfies the condition.
622
625
  *
@@ -736,6 +739,38 @@ var BPTree = class _BPTree {
736
739
  this._cachedRegexp.clear();
737
740
  this.nodes.clear();
738
741
  }
742
+ registerTransaction(txId) {
743
+ this.activeTransactions.add(txId);
744
+ }
745
+ unregisterTransaction(txId) {
746
+ this.activeTransactions.delete(txId);
747
+ }
748
+ pruneObsoleteNodes() {
749
+ if (this.activeTransactions.size === 0) {
750
+ this.sharedDeleteCache.clear();
751
+ return;
752
+ }
753
+ const minActiveTxId = Math.min(...this.activeTransactions);
754
+ for (const [id, entry] of this.sharedDeleteCache) {
755
+ if (entry.obsoleteAt < minActiveTxId) {
756
+ this.sharedDeleteCache.delete(id);
757
+ }
758
+ }
759
+ }
760
+ getObsoleteNode(id) {
761
+ return this.sharedDeleteCache.get(id)?.node;
762
+ }
763
+ addObsoleteNode(node, obsoleteAt) {
764
+ this.sharedDeleteCache.set(node.id, { node, obsoleteAt });
765
+ }
766
+ getNextTransactionId() {
767
+ let nextId = Date.now();
768
+ if (nextId <= this.lastTransactionId) {
769
+ nextId = this.lastTransactionId + 1e-3;
770
+ }
771
+ this.lastTransactionId = nextId;
772
+ return nextId;
773
+ }
739
774
  };
740
775
 
741
776
  // src/base/BPTreeSyncBase.ts
@@ -1324,7 +1359,9 @@ var BPTreeSyncBase = class extends BPTree {
1324
1359
  this._nodeUpdateBuffer.clear();
1325
1360
  }
1326
1361
  commitNodeDeleteBuffer() {
1362
+ const obsoleteAt = this.getNextTransactionId();
1327
1363
  for (const node of this._nodeDeleteBuffer.values()) {
1364
+ this.addObsoleteNode(node, obsoleteAt);
1328
1365
  this.strategy.delete(node.id);
1329
1366
  this.nodes.delete(node.id);
1330
1367
  }
@@ -1486,6 +1523,7 @@ var BPTreeSyncBase = class extends BPTree {
1486
1523
  var SerializeStrategy = class {
1487
1524
  order;
1488
1525
  head;
1526
+ lastCommittedTransactionId = 0;
1489
1527
  constructor(order) {
1490
1528
  this.order = order;
1491
1529
  this.head = {
@@ -1514,13 +1552,13 @@ var SerializeStrategySync = class extends SerializeStrategy {
1514
1552
  this.setHeadData(key, next);
1515
1553
  return current;
1516
1554
  }
1517
- compareAndSwapHead(oldRoot, newRoot) {
1518
- if (this.head.root !== oldRoot) {
1519
- return false;
1520
- }
1555
+ getLastCommittedTransactionId() {
1556
+ return this.lastCommittedTransactionId;
1557
+ }
1558
+ compareAndSwapHead(newRoot, newTxId) {
1521
1559
  this.head.root = newRoot;
1560
+ this.lastCommittedTransactionId = newTxId;
1522
1561
  this.writeHead(this.head);
1523
- return true;
1524
1562
  }
1525
1563
  };
1526
1564
  var InMemoryStoreStrategySync = class extends SerializeStrategySync {
@@ -1589,8 +1627,11 @@ var BPTreeSyncSnapshotStrategy = class extends SerializeStrategySync {
1589
1627
  this.snapshotHead.root = head.root;
1590
1628
  this.snapshotHead.data = { ...head.data };
1591
1629
  }
1592
- compareAndSwapHead(oldRoot, newRoot) {
1593
- return this.baseStrategy.compareAndSwapHead(oldRoot, newRoot);
1630
+ compareAndSwapHead(newRoot, newTxId) {
1631
+ this.baseStrategy.compareAndSwapHead(newRoot, newTxId);
1632
+ }
1633
+ getLastCommittedTransactionId() {
1634
+ return this.baseStrategy.getLastCommittedTransactionId();
1594
1635
  }
1595
1636
  getHeadData(key, defaultValue) {
1596
1637
  return this.snapshotHead.data[key] ?? defaultValue;
@@ -1611,8 +1652,12 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1611
1652
  dirtyIds;
1612
1653
  createdInTx;
1613
1654
  deletedIds;
1655
+ obsoleteNodes = /* @__PURE__ */ new Map();
1656
+ originalNodes = /* @__PURE__ */ new Map();
1614
1657
  initialRootId;
1615
1658
  transactionRootId;
1659
+ transactionId;
1660
+ initialLastCommittedTransactionId = 0;
1616
1661
  constructor(baseTree) {
1617
1662
  super(baseTree.strategy, baseTree.comparator, baseTree.option);
1618
1663
  this.realBaseTree = baseTree;
@@ -1623,6 +1668,7 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1623
1668
  this.dirtyIds = /* @__PURE__ */ new Set();
1624
1669
  this.createdInTx = /* @__PURE__ */ new Set();
1625
1670
  this.deletedIds = /* @__PURE__ */ new Set();
1671
+ this.transactionId = Date.now() + Math.random();
1626
1672
  }
1627
1673
  /**
1628
1674
  * Initializes the transaction by capturing the current state of the tree.
@@ -1639,6 +1685,7 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1639
1685
  const root = this._createNode(true, [], [], true);
1640
1686
  this.initialRootId = root.id;
1641
1687
  }
1688
+ this.initialLastCommittedTransactionId = this.realBaseStrategy.getLastCommittedTransactionId();
1642
1689
  this.transactionRootId = this.initialRootId;
1643
1690
  this.rootId = this.transactionRootId;
1644
1691
  const snapshotStrategy = new BPTreeSyncSnapshotStrategy(this.realBaseStrategy, this.initialRootId);
@@ -1647,6 +1694,7 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1647
1694
  this.dirtyIds.clear();
1648
1695
  this.createdInTx.clear();
1649
1696
  this.deletedIds.clear();
1697
+ this.realBaseTree.registerTransaction(this.transactionId);
1650
1698
  }
1651
1699
  getNode(id) {
1652
1700
  if (this.txNodes.has(id)) {
@@ -1655,7 +1703,13 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1655
1703
  if (this.deletedIds.has(id)) {
1656
1704
  throw new Error(`The tree attempted to reference deleted node '${id}'`);
1657
1705
  }
1658
- const baseNode = this.realBaseStrategy.read(id);
1706
+ let baseNode = this.realBaseTree.getObsoleteNode(id);
1707
+ if (!baseNode) {
1708
+ baseNode = this.realBaseStrategy.read(id);
1709
+ }
1710
+ if (!this.originalNodes.has(id) && !this.createdInTx.has(id)) {
1711
+ this.originalNodes.set(id, JSON.parse(JSON.stringify(baseNode)));
1712
+ }
1659
1713
  const clone = JSON.parse(JSON.stringify(baseNode));
1660
1714
  this.txNodes.set(id, clone);
1661
1715
  return clone;
@@ -1745,9 +1799,10 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1745
1799
  * Attempts to commit the transaction.
1746
1800
  * Uses Optimistic Locking (Compare-And-Swap) on the root node ID to detect conflicts.
1747
1801
  *
1802
+ * @param cleanup Whether to clean up obsolete nodes after commit. Defaults to true.
1748
1803
  * @returns The transaction result.
1749
1804
  */
1750
- commit() {
1805
+ commit(cleanup = true) {
1751
1806
  const idMapping = /* @__PURE__ */ new Map();
1752
1807
  const finalNodes = [];
1753
1808
  for (const oldId of this.dirtyIds) {
@@ -1795,24 +1850,47 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1795
1850
  if (idMapping.has(this.rootId)) {
1796
1851
  newRootId = idMapping.get(this.rootId);
1797
1852
  }
1798
- for (const node of finalNodes) {
1799
- this.realBaseStrategy.write(node.id, node);
1853
+ let success = false;
1854
+ if (finalNodes.length === 0) {
1855
+ success = true;
1856
+ } else if (this.realBaseStrategy.getLastCommittedTransactionId() === this.initialLastCommittedTransactionId) {
1857
+ for (const node of finalNodes) {
1858
+ this.realBaseStrategy.write(node.id, node);
1859
+ }
1860
+ this.realBaseStrategy.compareAndSwapHead(newRootId, this.transactionId);
1861
+ success = true;
1800
1862
  }
1801
- const success = this.realBaseStrategy.compareAndSwapHead(this.initialRootId, newRootId);
1802
1863
  if (success) {
1803
1864
  const distinctObsolete = /* @__PURE__ */ new Set();
1804
1865
  for (const oldId of this.dirtyIds) {
1805
- if (!this.createdInTx.has(oldId) && this.txNodes.has(oldId)) {
1806
- distinctObsolete.add(oldId);
1866
+ if (!this.createdInTx.has(oldId)) {
1867
+ if (this.txNodes.has(oldId) || this.deletedIds.has(oldId)) {
1868
+ distinctObsolete.add(oldId);
1869
+ if (this.originalNodes.has(oldId)) {
1870
+ this.obsoleteNodes.set(oldId, this.originalNodes.get(oldId));
1871
+ }
1872
+ }
1807
1873
  }
1808
1874
  }
1875
+ if (cleanup) {
1876
+ for (const obsoleteId of distinctObsolete) {
1877
+ if (this.originalNodes.has(obsoleteId)) {
1878
+ this.realBaseTree.addObsoleteNode(
1879
+ this.originalNodes.get(obsoleteId),
1880
+ this.transactionId
1881
+ );
1882
+ }
1883
+ this.realBaseStrategy.delete(obsoleteId);
1884
+ }
1885
+ }
1886
+ this.realBaseTree.unregisterTransaction(this.transactionId);
1809
1887
  return {
1810
1888
  success: true,
1811
1889
  createdIds: newCreatedIds,
1812
1890
  obsoleteIds: Array.from(distinctObsolete)
1813
1891
  };
1814
1892
  } else {
1815
- this.rollback();
1893
+ this.rollback(cleanup);
1816
1894
  return {
1817
1895
  success: false,
1818
1896
  createdIds: newCreatedIds,
@@ -1836,8 +1914,15 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1836
1914
  this.realBaseStrategy.delete(id);
1837
1915
  }
1838
1916
  }
1917
+ this.realBaseTree.unregisterTransaction(this.transactionId);
1839
1918
  return createdIds;
1840
1919
  }
1920
+ readLock(fn) {
1921
+ return fn();
1922
+ }
1923
+ writeLock(fn) {
1924
+ return fn();
1925
+ }
1841
1926
  // Override to do nothing, as transaction handles its own commits
1842
1927
  commitHeadBuffer() {
1843
1928
  }
@@ -2475,7 +2560,9 @@ var BPTreeAsyncBase = class extends BPTree {
2475
2560
  this._nodeUpdateBuffer.clear();
2476
2561
  }
2477
2562
  async commitNodeDeleteBuffer() {
2563
+ const obsoleteAt = this.getNextTransactionId();
2478
2564
  for (const node of this._nodeDeleteBuffer.values()) {
2565
+ this.addObsoleteNode(node, obsoleteAt);
2479
2566
  await this.strategy.delete(node.id);
2480
2567
  this.nodes.delete(node.id);
2481
2568
  }
@@ -2640,8 +2727,276 @@ var BPTreeAsyncBase = class extends BPTree {
2640
2727
  }
2641
2728
  };
2642
2729
 
2730
+ // node_modules/ryoiki/dist/esm/index.mjs
2731
+ var Ryoiki = class _Ryoiki {
2732
+ readings;
2733
+ writings;
2734
+ readQueue;
2735
+ writeQueue;
2736
+ static async CatchError(promise) {
2737
+ return await promise.then((v) => [void 0, v]).catch((err) => [err]);
2738
+ }
2739
+ static IsRangeOverlap(a, b) {
2740
+ const [start1, end1] = a;
2741
+ const [start2, end2] = b;
2742
+ if (end1 <= start2 || end2 <= start1) {
2743
+ return false;
2744
+ }
2745
+ return true;
2746
+ }
2747
+ static ERR_ALREADY_EXISTS(lockId) {
2748
+ return new Error(`The '${lockId}' task already existing in queue or running.`);
2749
+ }
2750
+ static ERR_NOT_EXISTS(lockId) {
2751
+ return new Error(`The '${lockId}' task not existing in task queue.`);
2752
+ }
2753
+ static ERR_TIMEOUT(lockId, timeout) {
2754
+ return new Error(`The task with ID '${lockId}' failed to acquire the lock within the timeout(${timeout}ms).`);
2755
+ }
2756
+ /**
2757
+ * Constructs a new instance of the Ryoiki class.
2758
+ */
2759
+ constructor() {
2760
+ this.readings = /* @__PURE__ */ new Map();
2761
+ this.writings = /* @__PURE__ */ new Map();
2762
+ this.readQueue = /* @__PURE__ */ new Map();
2763
+ this.writeQueue = /* @__PURE__ */ new Map();
2764
+ }
2765
+ /**
2766
+ * Creates a range based on a start value and length.
2767
+ * @param start - The starting value of the range.
2768
+ * @param length - The length of the range.
2769
+ * @returns A range tuple [start, start + length].
2770
+ */
2771
+ range(start, length) {
2772
+ return [start, start + length];
2773
+ }
2774
+ rangeOverlapping(tasks, range) {
2775
+ return Array.from(tasks.values()).some((t) => _Ryoiki.IsRangeOverlap(t.range, range));
2776
+ }
2777
+ isSameRange(a, b) {
2778
+ const [a1, a2] = a;
2779
+ const [b1, b2] = b;
2780
+ return a1 === b1 && a2 === b2;
2781
+ }
2782
+ fetchUnitAndRun(queue, workspaces) {
2783
+ for (const [id, unit] of queue) {
2784
+ if (!unit.condition()) {
2785
+ continue;
2786
+ }
2787
+ this._alloc(queue, workspaces, id);
2788
+ }
2789
+ }
2790
+ _handleOverload(args, handlers, argPatterns) {
2791
+ for (const [key, pattern] of Object.entries(argPatterns)) {
2792
+ if (this._matchArgs(args, pattern)) {
2793
+ return handlers[key](...args);
2794
+ }
2795
+ }
2796
+ throw new Error("Invalid arguments");
2797
+ }
2798
+ _matchArgs(args, pattern) {
2799
+ return args.every((arg, index) => {
2800
+ const expectedType = pattern[index];
2801
+ if (expectedType === void 0) return typeof arg === "undefined";
2802
+ if (expectedType === Function) return typeof arg === "function";
2803
+ if (expectedType === Number) return typeof arg === "number";
2804
+ if (expectedType === Array) return Array.isArray(arg);
2805
+ return false;
2806
+ });
2807
+ }
2808
+ _createRandomId() {
2809
+ const timestamp = Date.now().toString(36);
2810
+ const random = Math.random().toString(36).substring(2);
2811
+ return `${timestamp}${random}`;
2812
+ }
2813
+ _alloc(queue, workspaces, lockId) {
2814
+ const unit = queue.get(lockId);
2815
+ if (!unit) {
2816
+ throw _Ryoiki.ERR_NOT_EXISTS(lockId);
2817
+ }
2818
+ workspaces.set(lockId, unit);
2819
+ queue.delete(lockId);
2820
+ unit.alloc();
2821
+ }
2822
+ _free(workspaces, lockId) {
2823
+ const unit = workspaces.get(lockId);
2824
+ if (!unit) {
2825
+ throw _Ryoiki.ERR_NOT_EXISTS(lockId);
2826
+ }
2827
+ workspaces.delete(lockId);
2828
+ unit.free();
2829
+ }
2830
+ _lock(queue, range, timeout, task, condition) {
2831
+ return new Promise((resolve, reject) => {
2832
+ let timeoutId = null;
2833
+ if (timeout >= 0) {
2834
+ timeoutId = setTimeout(() => {
2835
+ reject(_Ryoiki.ERR_TIMEOUT(id, timeout));
2836
+ }, timeout);
2837
+ }
2838
+ const id = this._createRandomId();
2839
+ const alloc = async () => {
2840
+ if (timeoutId !== null) {
2841
+ clearTimeout(timeoutId);
2842
+ }
2843
+ const [err, v] = await _Ryoiki.CatchError(task(id));
2844
+ if (err) reject(err);
2845
+ else resolve(v);
2846
+ };
2847
+ const fetch = () => {
2848
+ this.fetchUnitAndRun(this.readQueue, this.readings);
2849
+ this.fetchUnitAndRun(this.writeQueue, this.writings);
2850
+ };
2851
+ queue.set(id, { id, range, condition, alloc, free: fetch });
2852
+ fetch();
2853
+ });
2854
+ }
2855
+ _checkWorking(range, workspaces) {
2856
+ let isLocked = false;
2857
+ for (const lock of workspaces.values()) {
2858
+ if (_Ryoiki.IsRangeOverlap(range, lock.range)) {
2859
+ isLocked = true;
2860
+ break;
2861
+ }
2862
+ }
2863
+ return isLocked;
2864
+ }
2865
+ /**
2866
+ * Checks if there is any active read lock within the specified range.
2867
+ * @param range The range to check for active read locks.
2868
+ * @returns `true` if there is an active read lock within the range, `false` otherwise.
2869
+ */
2870
+ isReading(range) {
2871
+ return this._checkWorking(range, this.readings);
2872
+ }
2873
+ /**
2874
+ * Checks if there is any active write lock within the specified range.
2875
+ * @param range The range to check for active write locks.
2876
+ * @returns `true` if there is an active write lock within the range, `false` otherwise.
2877
+ */
2878
+ isWriting(range) {
2879
+ return this._checkWorking(range, this.writings);
2880
+ }
2881
+ /**
2882
+ * Checks if a read lock can be acquired within the specified range.
2883
+ * @param range The range to check for read lock availability.
2884
+ * @returns `true` if a read lock can be acquired, `false` otherwise.
2885
+ */
2886
+ canRead(range) {
2887
+ const writing = this.isWriting(range);
2888
+ return !writing;
2889
+ }
2890
+ /**
2891
+ * Checks if a write lock can be acquired within the specified range.
2892
+ * @param range The range to check for write lock availability.
2893
+ * @returns `true` if a write lock can be acquired, `false` otherwise.
2894
+ */
2895
+ canWrite(range) {
2896
+ const reading = this.isReading(range);
2897
+ const writing = this.isWriting(range);
2898
+ return !reading && !writing;
2899
+ }
2900
+ /**
2901
+ * Internal implementation of the read lock. Handles both overloads.
2902
+ * @template T - The return type of the task.
2903
+ * @param arg0 - Either a range or a task callback.
2904
+ * If a range is provided, the task is the second argument.
2905
+ * @param arg1 - The task to execute, required if a range is provided.
2906
+ * @param arg2 - The timeout for acquiring the lock.
2907
+ * If the lock cannot be acquired within this period, an error will be thrown.
2908
+ * If this value is not provided, no timeout will be set.
2909
+ * @returns A promise resolving to the result of the task execution.
2910
+ */
2911
+ readLock(arg0, arg1, arg2) {
2912
+ const [range, task, timeout] = this._handleOverload(
2913
+ [arg0, arg1, arg2],
2914
+ {
2915
+ rangeTask: (range2, task2) => [range2, task2, -1],
2916
+ rangeTaskTimeout: (range2, task2, timeout2) => [range2, task2, timeout2],
2917
+ task: (task2) => [[-Infinity, Infinity], task2, -1],
2918
+ taskTimeout: (task2, timeout2) => [[-Infinity, Infinity], task2, timeout2]
2919
+ },
2920
+ {
2921
+ task: [Function],
2922
+ taskTimeout: [Function, Number],
2923
+ rangeTask: [Array, Function],
2924
+ rangeTaskTimeout: [Array, Function, Number]
2925
+ }
2926
+ );
2927
+ return this._lock(
2928
+ this.readQueue,
2929
+ range,
2930
+ timeout,
2931
+ task,
2932
+ () => !this.rangeOverlapping(this.writings, range)
2933
+ );
2934
+ }
2935
+ /**
2936
+ * Internal implementation of the write 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
+ writeLock(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.writeQueue,
2964
+ range,
2965
+ timeout,
2966
+ task,
2967
+ () => {
2968
+ return !this.rangeOverlapping(this.writings, range) && !this.rangeOverlapping(this.readings, range);
2969
+ }
2970
+ );
2971
+ }
2972
+ /**
2973
+ * Releases a read lock by its lock ID.
2974
+ * @param lockId - The unique identifier for the lock to release.
2975
+ */
2976
+ readUnlock(lockId) {
2977
+ this._free(this.readings, lockId);
2978
+ }
2979
+ /**
2980
+ * Releases a write lock by its lock ID.
2981
+ * @param lockId - The unique identifier for the lock to release.
2982
+ */
2983
+ writeUnlock(lockId) {
2984
+ this._free(this.writings, lockId);
2985
+ }
2986
+ };
2987
+
2643
2988
  // src/SerializeStrategyAsync.ts
2644
2989
  var SerializeStrategyAsync = class extends SerializeStrategy {
2990
+ lock = new Ryoiki();
2991
+ async acquireLock(action) {
2992
+ let lockId;
2993
+ return this.lock.writeLock((_lockId) => {
2994
+ lockId = _lockId;
2995
+ return action();
2996
+ }).finally(() => {
2997
+ this.lock.writeUnlock(lockId);
2998
+ });
2999
+ }
2645
3000
  async getHeadData(key, defaultValue) {
2646
3001
  if (!Object.hasOwn(this.head.data, key)) {
2647
3002
  await this.setHeadData(key, defaultValue);
@@ -2658,13 +3013,13 @@ var SerializeStrategyAsync = class extends SerializeStrategy {
2658
3013
  await this.setHeadData(key, next);
2659
3014
  return current;
2660
3015
  }
2661
- async compareAndSwapHead(oldRoot, newRoot) {
2662
- if (this.head.root !== oldRoot) {
2663
- return false;
2664
- }
3016
+ async getLastCommittedTransactionId() {
3017
+ return this.lastCommittedTransactionId;
3018
+ }
3019
+ async compareAndSwapHead(newRoot, newTxId) {
2665
3020
  this.head.root = newRoot;
3021
+ this.lastCommittedTransactionId = newTxId;
2666
3022
  await this.writeHead(this.head);
2667
- return true;
2668
3023
  }
2669
3024
  };
2670
3025
  var InMemoryStoreStrategyAsync = class extends SerializeStrategyAsync {
@@ -2733,8 +3088,11 @@ var BPTreeAsyncSnapshotStrategy = class extends SerializeStrategyAsync {
2733
3088
  this.snapshotHead.root = head.root;
2734
3089
  this.snapshotHead.data = { ...head.data };
2735
3090
  }
2736
- async compareAndSwapHead(oldRoot, newRoot) {
2737
- return await this.baseStrategy.compareAndSwapHead(oldRoot, newRoot);
3091
+ async compareAndSwapHead(newRoot, newTxId) {
3092
+ await this.baseStrategy.compareAndSwapHead(newRoot, newTxId);
3093
+ }
3094
+ async getLastCommittedTransactionId() {
3095
+ return await this.baseStrategy.getLastCommittedTransactionId();
2738
3096
  }
2739
3097
  async getHeadData(key, defaultValue) {
2740
3098
  return this.snapshotHead.data[key] ?? defaultValue;
@@ -2755,8 +3113,12 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2755
3113
  dirtyIds;
2756
3114
  createdInTx;
2757
3115
  deletedIds;
3116
+ obsoleteNodes = /* @__PURE__ */ new Map();
3117
+ originalNodes = /* @__PURE__ */ new Map();
2758
3118
  initialRootId;
2759
3119
  transactionRootId;
3120
+ transactionId;
3121
+ initialLastCommittedTransactionId = 0;
2760
3122
  constructor(baseTree) {
2761
3123
  super(baseTree.strategy, baseTree.comparator, baseTree.option);
2762
3124
  this.realBaseTree = baseTree;
@@ -2767,6 +3129,7 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2767
3129
  this.dirtyIds = /* @__PURE__ */ new Set();
2768
3130
  this.createdInTx = /* @__PURE__ */ new Set();
2769
3131
  this.deletedIds = /* @__PURE__ */ new Set();
3132
+ this.transactionId = Date.now() + Math.random();
2770
3133
  }
2771
3134
  /**
2772
3135
  * Initializes the transaction by capturing the current state of the tree.
@@ -2783,6 +3146,7 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2783
3146
  const root = await this._createNode(true, [], [], true);
2784
3147
  this.initialRootId = root.id;
2785
3148
  }
3149
+ this.initialLastCommittedTransactionId = await this.realBaseStrategy.getLastCommittedTransactionId();
2786
3150
  this.transactionRootId = this.initialRootId;
2787
3151
  this.rootId = this.transactionRootId;
2788
3152
  const snapshotStrategy = new BPTreeAsyncSnapshotStrategy(this.realBaseStrategy, this.initialRootId);
@@ -2791,6 +3155,7 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2791
3155
  this.dirtyIds.clear();
2792
3156
  this.createdInTx.clear();
2793
3157
  this.deletedIds.clear();
3158
+ this.realBaseTree.registerTransaction(this.transactionId);
2794
3159
  }
2795
3160
  async getNode(id) {
2796
3161
  if (this.txNodes.has(id)) {
@@ -2799,7 +3164,13 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2799
3164
  if (this.deletedIds.has(id)) {
2800
3165
  throw new Error(`The tree attempted to reference deleted node '${id}'`);
2801
3166
  }
2802
- const baseNode = await this.realBaseStrategy.read(id);
3167
+ let baseNode = this.realBaseTree.getObsoleteNode(id);
3168
+ if (!baseNode) {
3169
+ baseNode = await this.realBaseStrategy.read(id);
3170
+ }
3171
+ if (!this.originalNodes.has(id) && !this.createdInTx.has(id)) {
3172
+ this.originalNodes.set(id, JSON.parse(JSON.stringify(baseNode)));
3173
+ }
2803
3174
  const clone = JSON.parse(JSON.stringify(baseNode));
2804
3175
  this.txNodes.set(id, clone);
2805
3176
  return clone;
@@ -2889,9 +3260,10 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2889
3260
  * Attempts to commit the transaction.
2890
3261
  * Uses Optimistic Locking (Compare-And-Swap) on the root node ID to detect conflicts.
2891
3262
  *
3263
+ * @param cleanup Whether to clean up obsolete nodes after commit. Defaults to true.
2892
3264
  * @returns A promise that resolves to the transaction result.
2893
3265
  */
2894
- async commit() {
3266
+ async commit(cleanup = true) {
2895
3267
  const idMapping = /* @__PURE__ */ new Map();
2896
3268
  const finalNodes = [];
2897
3269
  for (const oldId of this.dirtyIds) {
@@ -2939,24 +3311,52 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2939
3311
  if (idMapping.has(this.rootId)) {
2940
3312
  newRootId = idMapping.get(this.rootId);
2941
3313
  }
2942
- for (const node of finalNodes) {
2943
- await this.realBaseStrategy.write(node.id, node);
3314
+ let success = false;
3315
+ if (finalNodes.length === 0) {
3316
+ success = true;
3317
+ } else {
3318
+ success = await this.realBaseStrategy.acquireLock(async () => {
3319
+ if (await this.realBaseStrategy.getLastCommittedTransactionId() === this.initialLastCommittedTransactionId) {
3320
+ for (const node of finalNodes) {
3321
+ await this.realBaseStrategy.write(node.id, node);
3322
+ }
3323
+ await this.realBaseStrategy.compareAndSwapHead(newRootId, this.transactionId);
3324
+ return true;
3325
+ }
3326
+ return false;
3327
+ });
2944
3328
  }
2945
- const success = await this.realBaseStrategy.compareAndSwapHead(this.initialRootId, newRootId);
2946
3329
  if (success) {
2947
3330
  const distinctObsolete = /* @__PURE__ */ new Set();
2948
3331
  for (const oldId of this.dirtyIds) {
2949
- if (!this.createdInTx.has(oldId) && this.txNodes.has(oldId)) {
2950
- distinctObsolete.add(oldId);
3332
+ if (!this.createdInTx.has(oldId)) {
3333
+ if (this.txNodes.has(oldId) || this.deletedIds.has(oldId)) {
3334
+ distinctObsolete.add(oldId);
3335
+ if (this.originalNodes.has(oldId)) {
3336
+ this.obsoleteNodes.set(oldId, this.originalNodes.get(oldId));
3337
+ }
3338
+ }
3339
+ }
3340
+ }
3341
+ if (cleanup) {
3342
+ for (const obsoleteId of distinctObsolete) {
3343
+ if (this.originalNodes.has(obsoleteId)) {
3344
+ this.realBaseTree.addObsoleteNode(
3345
+ this.originalNodes.get(obsoleteId),
3346
+ this.transactionId
3347
+ );
3348
+ }
3349
+ await this.realBaseStrategy.delete(obsoleteId);
2951
3350
  }
2952
3351
  }
3352
+ this.realBaseTree.unregisterTransaction(this.transactionId);
2953
3353
  return {
2954
3354
  success: true,
2955
3355
  createdIds: newCreatedIds,
2956
3356
  obsoleteIds: Array.from(distinctObsolete)
2957
3357
  };
2958
3358
  } else {
2959
- await this.rollback();
3359
+ await this.rollback(cleanup);
2960
3360
  return {
2961
3361
  success: false,
2962
3362
  createdIds: newCreatedIds,
@@ -2980,6 +3380,7 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2980
3380
  await this.realBaseStrategy.delete(id);
2981
3381
  }
2982
3382
  }
3383
+ this.realBaseTree.unregisterTransaction(this.transactionId);
2983
3384
  return createdIds;
2984
3385
  }
2985
3386
  async readLock(fn) {