dataply 0.0.16-alpha.6 → 0.0.16-alpha.8

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/dist/cjs/index.js CHANGED
@@ -52,7 +52,7 @@ __export(src_exports, {
52
52
  OverflowPageManager: () => OverflowPageManager,
53
53
  PageManager: () => PageManager,
54
54
  PageManagerFactory: () => PageManagerFactory,
55
- Ryoiki: () => Ryoiki,
55
+ Ryoiki: () => Ryoiki2,
56
56
  SerializeStrategyAsync: () => SerializeStrategyAsync,
57
57
  SerializeStrategySync: () => SerializeStrategySync,
58
58
  StringComparator: () => StringComparator,
@@ -499,24 +499,13 @@ var BPTree = class _BPTree {
499
499
  option;
500
500
  order;
501
501
  rootId;
502
- /**
503
- * Returns the ID of the root node.
504
- * @returns The root node ID.
505
- */
506
- getRootId() {
507
- return this.rootId;
508
- }
509
- /**
510
- * Returns the order of the B+Tree.
511
- * @returns The order of the tree.
512
- */
513
- getOrder() {
514
- return this.order;
515
- }
516
502
  _strategyDirty;
517
503
  _nodeCreateBuffer;
518
504
  _nodeUpdateBuffer;
519
505
  _nodeDeleteBuffer;
506
+ sharedDeleteCache = /* @__PURE__ */ new Map();
507
+ activeTransactions = /* @__PURE__ */ new Set();
508
+ lastTransactionId = 0;
520
509
  verifierMap = {
521
510
  gt: (nv, v) => this.comparator.isHigher(nv, v),
522
511
  gte: (nv, v) => this.comparator.isHigher(nv, v) || this.comparator.isSame(nv, v),
@@ -677,6 +666,20 @@ var BPTree = class _BPTree {
677
666
  }
678
667
  return best;
679
668
  }
669
+ /**
670
+ * Returns the ID of the root node.
671
+ * @returns The root node ID.
672
+ */
673
+ getRootId() {
674
+ return this.rootId;
675
+ }
676
+ /**
677
+ * Returns the order of the B+Tree.
678
+ * @returns The order of the tree.
679
+ */
680
+ getOrder() {
681
+ return this.order;
682
+ }
680
683
  /**
681
684
  * Verified if the value satisfies the condition.
682
685
  *
@@ -796,6 +799,38 @@ var BPTree = class _BPTree {
796
799
  this._cachedRegexp.clear();
797
800
  this.nodes.clear();
798
801
  }
802
+ registerTransaction(txId) {
803
+ this.activeTransactions.add(txId);
804
+ }
805
+ unregisterTransaction(txId) {
806
+ this.activeTransactions.delete(txId);
807
+ }
808
+ pruneObsoleteNodes() {
809
+ if (this.activeTransactions.size === 0) {
810
+ this.sharedDeleteCache.clear();
811
+ return;
812
+ }
813
+ const minActiveTxId = Math.min(...this.activeTransactions);
814
+ for (const [id, entry] of this.sharedDeleteCache) {
815
+ if (entry.obsoleteAt < minActiveTxId) {
816
+ this.sharedDeleteCache.delete(id);
817
+ }
818
+ }
819
+ }
820
+ getObsoleteNode(id) {
821
+ return this.sharedDeleteCache.get(id)?.node;
822
+ }
823
+ addObsoleteNode(node, obsoleteAt) {
824
+ this.sharedDeleteCache.set(node.id, { node, obsoleteAt });
825
+ }
826
+ getNextTransactionId() {
827
+ let nextId = Date.now();
828
+ if (nextId <= this.lastTransactionId) {
829
+ nextId = this.lastTransactionId + 1e-3;
830
+ }
831
+ this.lastTransactionId = nextId;
832
+ return nextId;
833
+ }
799
834
  };
800
835
  var BPTreeSyncBase = class extends BPTree {
801
836
  constructor(strategy, comparator, option) {
@@ -1382,7 +1417,9 @@ var BPTreeSyncBase = class extends BPTree {
1382
1417
  this._nodeUpdateBuffer.clear();
1383
1418
  }
1384
1419
  commitNodeDeleteBuffer() {
1420
+ const obsoleteAt = this.getNextTransactionId();
1385
1421
  for (const node of this._nodeDeleteBuffer.values()) {
1422
+ this.addObsoleteNode(node, obsoleteAt);
1386
1423
  this.strategy.delete(node.id);
1387
1424
  this.nodes.delete(node.id);
1388
1425
  }
@@ -1542,6 +1579,7 @@ var BPTreeSyncBase = class extends BPTree {
1542
1579
  var SerializeStrategy = class {
1543
1580
  order;
1544
1581
  head;
1582
+ lastCommittedTransactionId = 0;
1545
1583
  constructor(order) {
1546
1584
  this.order = order;
1547
1585
  this.head = {
@@ -1568,13 +1606,13 @@ var SerializeStrategySync = class extends SerializeStrategy {
1568
1606
  this.setHeadData(key, next);
1569
1607
  return current;
1570
1608
  }
1571
- compareAndSwapHead(oldRoot, newRoot) {
1572
- if (this.head.root !== oldRoot) {
1573
- return false;
1574
- }
1609
+ getLastCommittedTransactionId() {
1610
+ return this.lastCommittedTransactionId;
1611
+ }
1612
+ compareAndSwapHead(newRoot, newTxId) {
1575
1613
  this.head.root = newRoot;
1614
+ this.lastCommittedTransactionId = newTxId;
1576
1615
  this.writeHead(this.head);
1577
- return true;
1578
1616
  }
1579
1617
  };
1580
1618
  var InMemoryStoreStrategySync = class extends SerializeStrategySync {
@@ -1641,8 +1679,11 @@ var BPTreeSyncSnapshotStrategy = class extends SerializeStrategySync {
1641
1679
  this.snapshotHead.root = head.root;
1642
1680
  this.snapshotHead.data = { ...head.data };
1643
1681
  }
1644
- compareAndSwapHead(oldRoot, newRoot) {
1645
- return this.baseStrategy.compareAndSwapHead(oldRoot, newRoot);
1682
+ compareAndSwapHead(newRoot, newTxId) {
1683
+ this.baseStrategy.compareAndSwapHead(newRoot, newTxId);
1684
+ }
1685
+ getLastCommittedTransactionId() {
1686
+ return this.baseStrategy.getLastCommittedTransactionId();
1646
1687
  }
1647
1688
  getHeadData(key, defaultValue) {
1648
1689
  return this.snapshotHead.data[key] ?? defaultValue;
@@ -1661,8 +1702,11 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1661
1702
  dirtyIds;
1662
1703
  createdInTx;
1663
1704
  deletedIds;
1705
+ originalNodes = /* @__PURE__ */ new Map();
1664
1706
  initialRootId;
1665
1707
  transactionRootId;
1708
+ transactionId;
1709
+ initialLastCommittedTransactionId = 0;
1666
1710
  constructor(baseTree) {
1667
1711
  super(baseTree.strategy, baseTree.comparator, baseTree.option);
1668
1712
  this.realBaseTree = baseTree;
@@ -1673,6 +1717,7 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1673
1717
  this.dirtyIds = /* @__PURE__ */ new Set();
1674
1718
  this.createdInTx = /* @__PURE__ */ new Set();
1675
1719
  this.deletedIds = /* @__PURE__ */ new Set();
1720
+ this.transactionId = Date.now() + Math.random();
1676
1721
  }
1677
1722
  /**
1678
1723
  * Initializes the transaction by capturing the current state of the tree.
@@ -1689,6 +1734,7 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1689
1734
  const root = this._createNode(true, [], [], true);
1690
1735
  this.initialRootId = root.id;
1691
1736
  }
1737
+ this.initialLastCommittedTransactionId = this.realBaseStrategy.getLastCommittedTransactionId();
1692
1738
  this.transactionRootId = this.initialRootId;
1693
1739
  this.rootId = this.transactionRootId;
1694
1740
  const snapshotStrategy = new BPTreeSyncSnapshotStrategy(this.realBaseStrategy, this.initialRootId);
@@ -1697,6 +1743,7 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1697
1743
  this.dirtyIds.clear();
1698
1744
  this.createdInTx.clear();
1699
1745
  this.deletedIds.clear();
1746
+ this.realBaseTree.registerTransaction(this.transactionId);
1700
1747
  }
1701
1748
  getNode(id) {
1702
1749
  if (this.txNodes.has(id)) {
@@ -1705,7 +1752,13 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1705
1752
  if (this.deletedIds.has(id)) {
1706
1753
  throw new Error(`The tree attempted to reference deleted node '${id}'`);
1707
1754
  }
1708
- const baseNode = this.realBaseStrategy.read(id);
1755
+ let baseNode = this.realBaseTree.getObsoleteNode(id);
1756
+ if (!baseNode) {
1757
+ baseNode = this.realBaseStrategy.read(id);
1758
+ }
1759
+ if (!this.originalNodes.has(id) && !this.createdInTx.has(id)) {
1760
+ this.originalNodes.set(id, JSON.parse(JSON.stringify(baseNode)));
1761
+ }
1709
1762
  const clone = JSON.parse(JSON.stringify(baseNode));
1710
1763
  this.txNodes.set(id, clone);
1711
1764
  return clone;
@@ -1795,9 +1848,10 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1795
1848
  * Attempts to commit the transaction.
1796
1849
  * Uses Optimistic Locking (Compare-And-Swap) on the root node ID to detect conflicts.
1797
1850
  *
1851
+ * @param cleanup Whether to clean up obsolete nodes after commit. Defaults to true.
1798
1852
  * @returns The transaction result.
1799
1853
  */
1800
- commit() {
1854
+ commit(cleanup = true) {
1801
1855
  const idMapping = /* @__PURE__ */ new Map();
1802
1856
  const finalNodes = [];
1803
1857
  for (const oldId of this.dirtyIds) {
@@ -1845,24 +1899,46 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1845
1899
  if (idMapping.has(this.rootId)) {
1846
1900
  newRootId = idMapping.get(this.rootId);
1847
1901
  }
1848
- for (const node of finalNodes) {
1849
- this.realBaseStrategy.write(node.id, node);
1902
+ let success = false;
1903
+ if (finalNodes.length === 0) {
1904
+ success = true;
1905
+ } else if (this.realBaseStrategy.getLastCommittedTransactionId() === this.initialLastCommittedTransactionId) {
1906
+ for (const node of finalNodes) {
1907
+ this.realBaseStrategy.write(node.id, node);
1908
+ }
1909
+ this.realBaseStrategy.compareAndSwapHead(newRootId, this.transactionId);
1910
+ success = true;
1850
1911
  }
1851
- const success = this.realBaseStrategy.compareAndSwapHead(this.initialRootId, newRootId);
1852
1912
  if (success) {
1853
1913
  const distinctObsolete = /* @__PURE__ */ new Set();
1854
1914
  for (const oldId of this.dirtyIds) {
1855
- if (!this.createdInTx.has(oldId) && this.txNodes.has(oldId)) {
1915
+ if (this.createdInTx.has(oldId)) {
1916
+ continue;
1917
+ }
1918
+ if (this.txNodes.has(oldId) || this.deletedIds.has(oldId)) {
1856
1919
  distinctObsolete.add(oldId);
1857
1920
  }
1858
1921
  }
1922
+ if (cleanup) {
1923
+ for (const obsoleteId of distinctObsolete) {
1924
+ if (this.originalNodes.has(obsoleteId)) {
1925
+ this.realBaseTree.addObsoleteNode(
1926
+ this.originalNodes.get(obsoleteId),
1927
+ this.transactionId
1928
+ );
1929
+ }
1930
+ this.realBaseStrategy.delete(obsoleteId);
1931
+ }
1932
+ }
1933
+ this.realBaseTree.unregisterTransaction(this.transactionId);
1934
+ this.realBaseTree.pruneObsoleteNodes();
1859
1935
  return {
1860
1936
  success: true,
1861
1937
  createdIds: newCreatedIds,
1862
1938
  obsoleteIds: Array.from(distinctObsolete)
1863
1939
  };
1864
1940
  } else {
1865
- this.rollback();
1941
+ this.rollback(cleanup);
1866
1942
  return {
1867
1943
  success: false,
1868
1944
  createdIds: newCreatedIds,
@@ -1886,8 +1962,16 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
1886
1962
  this.realBaseStrategy.delete(id);
1887
1963
  }
1888
1964
  }
1965
+ this.realBaseTree.unregisterTransaction(this.transactionId);
1966
+ this.realBaseTree.pruneObsoleteNodes();
1889
1967
  return createdIds;
1890
1968
  }
1969
+ readLock(fn) {
1970
+ return fn();
1971
+ }
1972
+ writeLock(fn) {
1973
+ return fn();
1974
+ }
1891
1975
  // Override to do nothing, as transaction handles its own commits
1892
1976
  commitHeadBuffer() {
1893
1977
  }
@@ -2521,7 +2605,9 @@ var BPTreeAsyncBase = class extends BPTree {
2521
2605
  this._nodeUpdateBuffer.clear();
2522
2606
  }
2523
2607
  async commitNodeDeleteBuffer() {
2608
+ const obsoleteAt = this.getNextTransactionId();
2524
2609
  for (const node of this._nodeDeleteBuffer.values()) {
2610
+ this.addObsoleteNode(node, obsoleteAt);
2525
2611
  await this.strategy.delete(node.id);
2526
2612
  this.nodes.delete(node.id);
2527
2613
  }
@@ -2685,7 +2771,273 @@ var BPTreeAsyncBase = class extends BPTree {
2685
2771
  await this.strategy.writeHead(this.strategy.head);
2686
2772
  }
2687
2773
  };
2774
+ var Ryoiki = class _Ryoiki {
2775
+ readings;
2776
+ writings;
2777
+ readQueue;
2778
+ writeQueue;
2779
+ static async CatchError(promise) {
2780
+ return await promise.then((v) => [void 0, v]).catch((err) => [err]);
2781
+ }
2782
+ static IsRangeOverlap(a, b) {
2783
+ const [start1, end1] = a;
2784
+ const [start2, end2] = b;
2785
+ if (end1 <= start2 || end2 <= start1) {
2786
+ return false;
2787
+ }
2788
+ return true;
2789
+ }
2790
+ static ERR_ALREADY_EXISTS(lockId) {
2791
+ return new Error(`The '${lockId}' task already existing in queue or running.`);
2792
+ }
2793
+ static ERR_NOT_EXISTS(lockId) {
2794
+ return new Error(`The '${lockId}' task not existing in task queue.`);
2795
+ }
2796
+ static ERR_TIMEOUT(lockId, timeout) {
2797
+ return new Error(`The task with ID '${lockId}' failed to acquire the lock within the timeout(${timeout}ms).`);
2798
+ }
2799
+ /**
2800
+ * Constructs a new instance of the Ryoiki class.
2801
+ */
2802
+ constructor() {
2803
+ this.readings = /* @__PURE__ */ new Map();
2804
+ this.writings = /* @__PURE__ */ new Map();
2805
+ this.readQueue = /* @__PURE__ */ new Map();
2806
+ this.writeQueue = /* @__PURE__ */ new Map();
2807
+ }
2808
+ /**
2809
+ * Creates a range based on a start value and length.
2810
+ * @param start - The starting value of the range.
2811
+ * @param length - The length of the range.
2812
+ * @returns A range tuple [start, start + length].
2813
+ */
2814
+ range(start, length) {
2815
+ return [start, start + length];
2816
+ }
2817
+ rangeOverlapping(tasks, range) {
2818
+ return Array.from(tasks.values()).some((t) => _Ryoiki.IsRangeOverlap(t.range, range));
2819
+ }
2820
+ isSameRange(a, b) {
2821
+ const [a1, a2] = a;
2822
+ const [b1, b2] = b;
2823
+ return a1 === b1 && a2 === b2;
2824
+ }
2825
+ fetchUnitAndRun(queue, workspaces) {
2826
+ for (const [id, unit] of queue) {
2827
+ if (!unit.condition()) {
2828
+ continue;
2829
+ }
2830
+ this._alloc(queue, workspaces, id);
2831
+ }
2832
+ }
2833
+ _handleOverload(args, handlers, argPatterns) {
2834
+ for (const [key, pattern] of Object.entries(argPatterns)) {
2835
+ if (this._matchArgs(args, pattern)) {
2836
+ return handlers[key](...args);
2837
+ }
2838
+ }
2839
+ throw new Error("Invalid arguments");
2840
+ }
2841
+ _matchArgs(args, pattern) {
2842
+ return args.every((arg, index) => {
2843
+ const expectedType = pattern[index];
2844
+ if (expectedType === void 0) return typeof arg === "undefined";
2845
+ if (expectedType === Function) return typeof arg === "function";
2846
+ if (expectedType === Number) return typeof arg === "number";
2847
+ if (expectedType === Array) return Array.isArray(arg);
2848
+ return false;
2849
+ });
2850
+ }
2851
+ _createRandomId() {
2852
+ const timestamp = Date.now().toString(36);
2853
+ const random = Math.random().toString(36).substring(2);
2854
+ return `${timestamp}${random}`;
2855
+ }
2856
+ _alloc(queue, workspaces, lockId) {
2857
+ const unit = queue.get(lockId);
2858
+ if (!unit) {
2859
+ throw _Ryoiki.ERR_NOT_EXISTS(lockId);
2860
+ }
2861
+ workspaces.set(lockId, unit);
2862
+ queue.delete(lockId);
2863
+ unit.alloc();
2864
+ }
2865
+ _free(workspaces, lockId) {
2866
+ const unit = workspaces.get(lockId);
2867
+ if (!unit) {
2868
+ throw _Ryoiki.ERR_NOT_EXISTS(lockId);
2869
+ }
2870
+ workspaces.delete(lockId);
2871
+ unit.free();
2872
+ }
2873
+ _lock(queue, range, timeout, task, condition) {
2874
+ return new Promise((resolve, reject) => {
2875
+ let timeoutId = null;
2876
+ if (timeout >= 0) {
2877
+ timeoutId = setTimeout(() => {
2878
+ reject(_Ryoiki.ERR_TIMEOUT(id, timeout));
2879
+ }, timeout);
2880
+ }
2881
+ const id = this._createRandomId();
2882
+ const alloc = async () => {
2883
+ if (timeoutId !== null) {
2884
+ clearTimeout(timeoutId);
2885
+ }
2886
+ const [err, v] = await _Ryoiki.CatchError(task(id));
2887
+ if (err) reject(err);
2888
+ else resolve(v);
2889
+ };
2890
+ const fetch = () => {
2891
+ this.fetchUnitAndRun(this.readQueue, this.readings);
2892
+ this.fetchUnitAndRun(this.writeQueue, this.writings);
2893
+ };
2894
+ queue.set(id, { id, range, condition, alloc, free: fetch });
2895
+ fetch();
2896
+ });
2897
+ }
2898
+ _checkWorking(range, workspaces) {
2899
+ let isLocked = false;
2900
+ for (const lock of workspaces.values()) {
2901
+ if (_Ryoiki.IsRangeOverlap(range, lock.range)) {
2902
+ isLocked = true;
2903
+ break;
2904
+ }
2905
+ }
2906
+ return isLocked;
2907
+ }
2908
+ /**
2909
+ * Checks if there is any active read lock within the specified range.
2910
+ * @param range The range to check for active read locks.
2911
+ * @returns `true` if there is an active read lock within the range, `false` otherwise.
2912
+ */
2913
+ isReading(range) {
2914
+ return this._checkWorking(range, this.readings);
2915
+ }
2916
+ /**
2917
+ * Checks if there is any active write lock within the specified range.
2918
+ * @param range The range to check for active write locks.
2919
+ * @returns `true` if there is an active write lock within the range, `false` otherwise.
2920
+ */
2921
+ isWriting(range) {
2922
+ return this._checkWorking(range, this.writings);
2923
+ }
2924
+ /**
2925
+ * Checks if a read lock can be acquired within the specified range.
2926
+ * @param range The range to check for read lock availability.
2927
+ * @returns `true` if a read lock can be acquired, `false` otherwise.
2928
+ */
2929
+ canRead(range) {
2930
+ const writing = this.isWriting(range);
2931
+ return !writing;
2932
+ }
2933
+ /**
2934
+ * Checks if a write lock can be acquired within the specified range.
2935
+ * @param range The range to check for write lock availability.
2936
+ * @returns `true` if a write lock can be acquired, `false` otherwise.
2937
+ */
2938
+ canWrite(range) {
2939
+ const reading = this.isReading(range);
2940
+ const writing = this.isWriting(range);
2941
+ return !reading && !writing;
2942
+ }
2943
+ /**
2944
+ * Internal implementation of the read lock. Handles both overloads.
2945
+ * @template T - The return type of the task.
2946
+ * @param arg0 - Either a range or a task callback.
2947
+ * If a range is provided, the task is the second argument.
2948
+ * @param arg1 - The task to execute, required if a range is provided.
2949
+ * @param arg2 - The timeout for acquiring the lock.
2950
+ * If the lock cannot be acquired within this period, an error will be thrown.
2951
+ * If this value is not provided, no timeout will be set.
2952
+ * @returns A promise resolving to the result of the task execution.
2953
+ */
2954
+ readLock(arg0, arg1, arg2) {
2955
+ const [range, task, timeout] = this._handleOverload(
2956
+ [arg0, arg1, arg2],
2957
+ {
2958
+ rangeTask: (range2, task2) => [range2, task2, -1],
2959
+ rangeTaskTimeout: (range2, task2, timeout2) => [range2, task2, timeout2],
2960
+ task: (task2) => [[-Infinity, Infinity], task2, -1],
2961
+ taskTimeout: (task2, timeout2) => [[-Infinity, Infinity], task2, timeout2]
2962
+ },
2963
+ {
2964
+ task: [Function],
2965
+ taskTimeout: [Function, Number],
2966
+ rangeTask: [Array, Function],
2967
+ rangeTaskTimeout: [Array, Function, Number]
2968
+ }
2969
+ );
2970
+ return this._lock(
2971
+ this.readQueue,
2972
+ range,
2973
+ timeout,
2974
+ task,
2975
+ () => !this.rangeOverlapping(this.writings, range)
2976
+ );
2977
+ }
2978
+ /**
2979
+ * Internal implementation of the write lock. Handles both overloads.
2980
+ * @template T - The return type of the task.
2981
+ * @param arg0 - Either a range or a task callback.
2982
+ * If a range is provided, the task is the second argument.
2983
+ * @param arg1 - The task to execute, required if a range is provided.
2984
+ * @param arg2 - The timeout for acquiring the lock.
2985
+ * If the lock cannot be acquired within this period, an error will be thrown.
2986
+ * If this value is not provided, no timeout will be set.
2987
+ * @returns A promise resolving to the result of the task execution.
2988
+ */
2989
+ writeLock(arg0, arg1, arg2) {
2990
+ const [range, task, timeout] = this._handleOverload(
2991
+ [arg0, arg1, arg2],
2992
+ {
2993
+ rangeTask: (range2, task2) => [range2, task2, -1],
2994
+ rangeTaskTimeout: (range2, task2, timeout2) => [range2, task2, timeout2],
2995
+ task: (task2) => [[-Infinity, Infinity], task2, -1],
2996
+ taskTimeout: (task2, timeout2) => [[-Infinity, Infinity], task2, timeout2]
2997
+ },
2998
+ {
2999
+ task: [Function],
3000
+ taskTimeout: [Function, Number],
3001
+ rangeTask: [Array, Function],
3002
+ rangeTaskTimeout: [Array, Function, Number]
3003
+ }
3004
+ );
3005
+ return this._lock(
3006
+ this.writeQueue,
3007
+ range,
3008
+ timeout,
3009
+ task,
3010
+ () => {
3011
+ return !this.rangeOverlapping(this.writings, range) && !this.rangeOverlapping(this.readings, range);
3012
+ }
3013
+ );
3014
+ }
3015
+ /**
3016
+ * Releases a read lock by its lock ID.
3017
+ * @param lockId - The unique identifier for the lock to release.
3018
+ */
3019
+ readUnlock(lockId) {
3020
+ this._free(this.readings, lockId);
3021
+ }
3022
+ /**
3023
+ * Releases a write lock by its lock ID.
3024
+ * @param lockId - The unique identifier for the lock to release.
3025
+ */
3026
+ writeUnlock(lockId) {
3027
+ this._free(this.writings, lockId);
3028
+ }
3029
+ };
2688
3030
  var SerializeStrategyAsync = class extends SerializeStrategy {
3031
+ lock = new Ryoiki();
3032
+ async acquireLock(action) {
3033
+ let lockId;
3034
+ return this.lock.writeLock((_lockId) => {
3035
+ lockId = _lockId;
3036
+ return action();
3037
+ }).finally(() => {
3038
+ this.lock.writeUnlock(lockId);
3039
+ });
3040
+ }
2689
3041
  async getHeadData(key, defaultValue) {
2690
3042
  if (!Object.hasOwn(this.head.data, key)) {
2691
3043
  await this.setHeadData(key, defaultValue);
@@ -2702,13 +3054,13 @@ var SerializeStrategyAsync = class extends SerializeStrategy {
2702
3054
  await this.setHeadData(key, next);
2703
3055
  return current;
2704
3056
  }
2705
- async compareAndSwapHead(oldRoot, newRoot) {
2706
- if (this.head.root !== oldRoot) {
2707
- return false;
2708
- }
3057
+ async getLastCommittedTransactionId() {
3058
+ return this.lastCommittedTransactionId;
3059
+ }
3060
+ async compareAndSwapHead(newRoot, newTxId) {
2709
3061
  this.head.root = newRoot;
3062
+ this.lastCommittedTransactionId = newTxId;
2710
3063
  await this.writeHead(this.head);
2711
- return true;
2712
3064
  }
2713
3065
  };
2714
3066
  var InMemoryStoreStrategyAsync = class extends SerializeStrategyAsync {
@@ -2775,8 +3127,11 @@ var BPTreeAsyncSnapshotStrategy = class extends SerializeStrategyAsync {
2775
3127
  this.snapshotHead.root = head.root;
2776
3128
  this.snapshotHead.data = { ...head.data };
2777
3129
  }
2778
- async compareAndSwapHead(oldRoot, newRoot) {
2779
- return await this.baseStrategy.compareAndSwapHead(oldRoot, newRoot);
3130
+ async compareAndSwapHead(newRoot, newTxId) {
3131
+ await this.baseStrategy.compareAndSwapHead(newRoot, newTxId);
3132
+ }
3133
+ async getLastCommittedTransactionId() {
3134
+ return await this.baseStrategy.getLastCommittedTransactionId();
2780
3135
  }
2781
3136
  async getHeadData(key, defaultValue) {
2782
3137
  return this.snapshotHead.data[key] ?? defaultValue;
@@ -2795,8 +3150,11 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2795
3150
  dirtyIds;
2796
3151
  createdInTx;
2797
3152
  deletedIds;
3153
+ originalNodes = /* @__PURE__ */ new Map();
2798
3154
  initialRootId;
2799
3155
  transactionRootId;
3156
+ transactionId;
3157
+ initialLastCommittedTransactionId = 0;
2800
3158
  constructor(baseTree) {
2801
3159
  super(baseTree.strategy, baseTree.comparator, baseTree.option);
2802
3160
  this.realBaseTree = baseTree;
@@ -2807,6 +3165,7 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2807
3165
  this.dirtyIds = /* @__PURE__ */ new Set();
2808
3166
  this.createdInTx = /* @__PURE__ */ new Set();
2809
3167
  this.deletedIds = /* @__PURE__ */ new Set();
3168
+ this.transactionId = Date.now() + Math.random();
2810
3169
  }
2811
3170
  /**
2812
3171
  * Initializes the transaction by capturing the current state of the tree.
@@ -2823,6 +3182,7 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2823
3182
  const root = await this._createNode(true, [], [], true);
2824
3183
  this.initialRootId = root.id;
2825
3184
  }
3185
+ this.initialLastCommittedTransactionId = await this.realBaseStrategy.getLastCommittedTransactionId();
2826
3186
  this.transactionRootId = this.initialRootId;
2827
3187
  this.rootId = this.transactionRootId;
2828
3188
  const snapshotStrategy = new BPTreeAsyncSnapshotStrategy(this.realBaseStrategy, this.initialRootId);
@@ -2831,6 +3191,7 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2831
3191
  this.dirtyIds.clear();
2832
3192
  this.createdInTx.clear();
2833
3193
  this.deletedIds.clear();
3194
+ this.realBaseTree.registerTransaction(this.transactionId);
2834
3195
  }
2835
3196
  async getNode(id) {
2836
3197
  if (this.txNodes.has(id)) {
@@ -2839,7 +3200,13 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2839
3200
  if (this.deletedIds.has(id)) {
2840
3201
  throw new Error(`The tree attempted to reference deleted node '${id}'`);
2841
3202
  }
2842
- 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
+ }
2843
3210
  const clone = JSON.parse(JSON.stringify(baseNode));
2844
3211
  this.txNodes.set(id, clone);
2845
3212
  return clone;
@@ -2929,9 +3296,10 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2929
3296
  * Attempts to commit the transaction.
2930
3297
  * Uses Optimistic Locking (Compare-And-Swap) on the root node ID to detect conflicts.
2931
3298
  *
3299
+ * @param cleanup Whether to clean up obsolete nodes after commit. Defaults to true.
2932
3300
  * @returns A promise that resolves to the transaction result.
2933
3301
  */
2934
- async commit() {
3302
+ async commit(cleanup = true) {
2935
3303
  const idMapping = /* @__PURE__ */ new Map();
2936
3304
  const finalNodes = [];
2937
3305
  for (const oldId of this.dirtyIds) {
@@ -2979,24 +3347,51 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
2979
3347
  if (idMapping.has(this.rootId)) {
2980
3348
  newRootId = idMapping.get(this.rootId);
2981
3349
  }
2982
- for (const node of finalNodes) {
2983
- 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
+ });
2984
3364
  }
2985
- const success = await this.realBaseStrategy.compareAndSwapHead(this.initialRootId, newRootId);
2986
3365
  if (success) {
2987
3366
  const distinctObsolete = /* @__PURE__ */ new Set();
2988
3367
  for (const oldId of this.dirtyIds) {
2989
- if (!this.createdInTx.has(oldId) && this.txNodes.has(oldId)) {
3368
+ if (this.createdInTx.has(oldId)) {
3369
+ continue;
3370
+ }
3371
+ if (this.txNodes.has(oldId) || this.deletedIds.has(oldId)) {
2990
3372
  distinctObsolete.add(oldId);
2991
3373
  }
2992
3374
  }
3375
+ if (cleanup) {
3376
+ for (const obsoleteId of distinctObsolete) {
3377
+ if (this.originalNodes.has(obsoleteId)) {
3378
+ this.realBaseTree.addObsoleteNode(
3379
+ this.originalNodes.get(obsoleteId),
3380
+ this.transactionId
3381
+ );
3382
+ }
3383
+ await this.realBaseStrategy.delete(obsoleteId);
3384
+ }
3385
+ }
3386
+ this.realBaseTree.unregisterTransaction(this.transactionId);
3387
+ this.realBaseTree.pruneObsoleteNodes();
2993
3388
  return {
2994
3389
  success: true,
2995
3390
  createdIds: newCreatedIds,
2996
3391
  obsoleteIds: Array.from(distinctObsolete)
2997
3392
  };
2998
3393
  } else {
2999
- await this.rollback();
3394
+ await this.rollback(cleanup);
3000
3395
  return {
3001
3396
  success: false,
3002
3397
  createdIds: newCreatedIds,
@@ -3020,6 +3415,8 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
3020
3415
  await this.realBaseStrategy.delete(id);
3021
3416
  }
3022
3417
  }
3418
+ this.realBaseTree.unregisterTransaction(this.transactionId);
3419
+ this.realBaseTree.pruneObsoleteNodes();
3023
3420
  return createdIds;
3024
3421
  }
3025
3422
  async readLock(fn) {
@@ -3077,7 +3474,7 @@ var BPTreeAsync = class extends BPTreeAsyncBase {
3077
3474
  };
3078
3475
 
3079
3476
  // node_modules/ryoiki/dist/esm/index.mjs
3080
- var Ryoiki = class _Ryoiki {
3477
+ var Ryoiki2 = class _Ryoiki2 {
3081
3478
  readings;
3082
3479
  writings;
3083
3480
  readQueue;
@@ -3121,7 +3518,7 @@ var Ryoiki = class _Ryoiki {
3121
3518
  return [start, start + length];
3122
3519
  }
3123
3520
  rangeOverlapping(tasks, range) {
3124
- return Array.from(tasks.values()).some((t) => _Ryoiki.IsRangeOverlap(t.range, range));
3521
+ return Array.from(tasks.values()).some((t) => _Ryoiki2.IsRangeOverlap(t.range, range));
3125
3522
  }
3126
3523
  isSameRange(a, b) {
3127
3524
  const [a1, a2] = a;
@@ -3162,7 +3559,7 @@ var Ryoiki = class _Ryoiki {
3162
3559
  _alloc(queue, workspaces, lockId) {
3163
3560
  const unit = queue.get(lockId);
3164
3561
  if (!unit) {
3165
- throw _Ryoiki.ERR_NOT_EXISTS(lockId);
3562
+ throw _Ryoiki2.ERR_NOT_EXISTS(lockId);
3166
3563
  }
3167
3564
  workspaces.set(lockId, unit);
3168
3565
  queue.delete(lockId);
@@ -3171,7 +3568,7 @@ var Ryoiki = class _Ryoiki {
3171
3568
  _free(workspaces, lockId) {
3172
3569
  const unit = workspaces.get(lockId);
3173
3570
  if (!unit) {
3174
- throw _Ryoiki.ERR_NOT_EXISTS(lockId);
3571
+ throw _Ryoiki2.ERR_NOT_EXISTS(lockId);
3175
3572
  }
3176
3573
  workspaces.delete(lockId);
3177
3574
  unit.free();
@@ -3181,7 +3578,7 @@ var Ryoiki = class _Ryoiki {
3181
3578
  let timeoutId = null;
3182
3579
  if (timeout >= 0) {
3183
3580
  timeoutId = setTimeout(() => {
3184
- reject(_Ryoiki.ERR_TIMEOUT(id, timeout));
3581
+ reject(_Ryoiki2.ERR_TIMEOUT(id, timeout));
3185
3582
  }, timeout);
3186
3583
  }
3187
3584
  const id = this._createRandomId();
@@ -3189,7 +3586,7 @@ var Ryoiki = class _Ryoiki {
3189
3586
  if (timeoutId !== null) {
3190
3587
  clearTimeout(timeoutId);
3191
3588
  }
3192
- const [err, v] = await _Ryoiki.CatchError(task(id));
3589
+ const [err, v] = await _Ryoiki2.CatchError(task(id));
3193
3590
  if (err) reject(err);
3194
3591
  else resolve(v);
3195
3592
  };
@@ -3204,7 +3601,7 @@ var Ryoiki = class _Ryoiki {
3204
3601
  _checkWorking(range, workspaces) {
3205
3602
  let isLocked = false;
3206
3603
  for (const lock of workspaces.values()) {
3207
- if (_Ryoiki.IsRangeOverlap(range, lock.range)) {
3604
+ if (_Ryoiki2.IsRangeOverlap(range, lock.range)) {
3208
3605
  isLocked = true;
3209
3606
  break;
3210
3607
  }
@@ -5614,7 +6011,6 @@ var VirtualFileSystem = class {
5614
6011
  this.fileSize = import_node_fs2.default.fstatSync(fileHandle).size;
5615
6012
  if (walPath) {
5616
6013
  this.logManager = new LogManager(walPath, pageSize);
5617
- this.recover();
5618
6014
  }
5619
6015
  }
5620
6016
  /** Cache list (Page ID -> Data Buffer) */
@@ -5633,10 +6029,9 @@ var VirtualFileSystem = class {
5633
6029
  activeTransactions = /* @__PURE__ */ new Map();
5634
6030
  /**
5635
6031
  * Performs recovery (Redo) using WAL logs.
5636
- * Called in constructor, so it's a synchronous process and data is only reflected in cache.
5637
- * Actual disk sync and log clearing are performed during future transactions or closure.
6032
+ * Called during initialization (DataplyAPI.init), ensuring data is fully restored before operations start.
5638
6033
  */
5639
- recover() {
6034
+ async recover() {
5640
6035
  if (!this.logManager) return;
5641
6036
  this.logManager.open();
5642
6037
  const restoredPages = this.logManager.readAllSync();
@@ -5669,11 +6064,10 @@ var VirtualFileSystem = class {
5669
6064
  this.fileSize = endPos;
5670
6065
  }
5671
6066
  }
5672
- Promise.all(promises).then(() => {
5673
- if (this.logManager && restoredPages.size > 0) {
5674
- this.logManager.clear().catch(console.error);
5675
- }
5676
- });
6067
+ await Promise.all(promises);
6068
+ if (this.logManager && restoredPages.size > 0) {
6069
+ await this.logManager.clear();
6070
+ }
5677
6071
  }
5678
6072
  /**
5679
6073
  * Prepares the transaction for commit (Phase 1).
@@ -5940,6 +6334,13 @@ var PageFileSystem = class {
5940
6334
  pageFactory = new PageManagerFactory();
5941
6335
  vfs;
5942
6336
  pageManagerFactory;
6337
+ /**
6338
+ * Initializes the page file system.
6339
+ * Performs VFS recovery if necessary.
6340
+ */
6341
+ async init() {
6342
+ await this.vfs.recover();
6343
+ }
5943
6344
  /**
5944
6345
  * Updates the bitmap status for a specific page.
5945
6346
  * @param pageId The ID of the page to update
@@ -6481,13 +6882,8 @@ var RowTableEngine = class {
6481
6882
  }
6482
6883
  if (!btx) return;
6483
6884
  const result = await btx.commit();
6484
- if (result.success) {
6485
- await this.bptree.init();
6486
- for (const id of result.obsoleteIds) {
6487
- await this.strategy.delete(id);
6488
- }
6489
- } else {
6490
- throw new Error(`BPTree transaction commit failed. Current Root: ${this.bptree.getRootId()}`);
6885
+ if (!result.success) {
6886
+ throw new Error(`BPTree transaction commit failed. Current TxID: ${btx.transactionId}`);
6491
6887
  }
6492
6888
  });
6493
6889
  }
@@ -6854,7 +7250,7 @@ var LockManager = class {
6854
7250
  lock;
6855
7251
  unlockMap = /* @__PURE__ */ new Map();
6856
7252
  constructor() {
6857
- this.lock = new Ryoiki();
7253
+ this.lock = new Ryoiki2();
6858
7254
  }
6859
7255
  /**
6860
7256
  * Requests a read (Shared) lock for a page.
@@ -7266,6 +7662,7 @@ var DataplyAPI = class {
7266
7662
  }
7267
7663
  await this.runWithDefault(async (tx) => {
7268
7664
  await this.hook.trigger("init", tx, async (tx2) => {
7665
+ await this.pfs.init();
7269
7666
  await this.rowTableEngine.init();
7270
7667
  this.initialized = true;
7271
7668
  return tx2;
@@ -21,6 +21,11 @@ export declare class PageFileSystem {
21
21
  * @param walPath WAL 파일 경로 (기본값: null)
22
22
  */
23
23
  constructor(fileHandle: number, pageSize: number, pageCacheCapacity: number, walPath?: string | undefined | null);
24
+ /**
25
+ * Initializes the page file system.
26
+ * Performs VFS recovery if necessary.
27
+ */
28
+ init(): Promise<void>;
24
29
  /**
25
30
  * Updates the bitmap status for a specific page.
26
31
  * @param pageId The ID of the page to update
@@ -22,10 +22,9 @@ export declare class VirtualFileSystem {
22
22
  constructor(fileHandle: number, pageSize: number, pageCacheCapacity: number, walPath?: string | undefined | null);
23
23
  /**
24
24
  * Performs recovery (Redo) using WAL logs.
25
- * Called in constructor, so it's a synchronous process and data is only reflected in cache.
26
- * Actual disk sync and log clearing are performed during future transactions or closure.
25
+ * Called during initialization (DataplyAPI.init), ensuring data is fully restored before operations start.
27
26
  */
28
- private recover;
27
+ recover(): Promise<void>;
29
28
  /**
30
29
  * Prepares the transaction for commit (Phase 1).
31
30
  * Writes dirty pages to WAL but does not update the main database file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dataply",
3
- "version": "0.0.16-alpha.6",
3
+ "version": "0.0.16-alpha.8",
4
4
  "description": "A lightweight storage engine for Node.js with support for MVCC, WAL.",
5
5
  "license": "MIT",
6
6
  "author": "izure <admin@izure.org>",
@@ -48,6 +48,6 @@
48
48
  "cache-entanglement": "^1.7.1",
49
49
  "hookall": "^2.2.0",
50
50
  "ryoiki": "^1.2.0",
51
- "serializable-bptree": "^7.0.2"
51
+ "serializable-bptree": "^7.0.4"
52
52
  }
53
- }
53
+ }
package/readme.md CHANGED
@@ -9,14 +9,15 @@
9
9
 
10
10
  ## Key Features
11
11
 
12
- - **🚀 Identity-Based Access**: Specialized in storing records and managing them via auto-generated Primary Keys.
13
- - **⚡ High-Performance B+Tree**: Optimizes data lookup and insertion through an asynchronous B+Tree structure.
14
- - **🛡️ MVCC Support**: Enables non-blocking read operations and guarantees data isolation between transactions.
15
- - **📝 WAL (Write-Ahead Logging)**: Ensures data integrity and provides recovery capabilities in case of system failures.
16
- - **💼 Transaction Mechanism**: Supports Commit and Rollback for atomic operations.
17
- - **📦 Page-Based Storage**: Efficient page caching and disk I/O optimization through Virtual File System (VFS).
18
- - **📉 Bitmap Space Optimization**: Uses bitmapped management to efficiently track page usage and maximize disk space utilization.
19
- - **⌨️ TypeScript Support**: Provides comprehensive type definitions for all APIs.
12
+ Dataply provides essential features for high-performance data management:
13
+
14
+ - **Identity-Based Access**: Manage records through auto-generated Primary Keys for ultra-fast retrieval.
15
+ - **High-Performance B+Tree**: Asynchronous B+Tree structure optimizes both lookups and insertions.
16
+ - **MVCC & Isolation**: Snapshot isolation via Multi-Version Concurrency Control (MVCC) enables non-blocking reads.
17
+ - **Reliability (WAL)**: Write-Ahead Logging (WAL) ensures data integrity and automatic crash recovery.
18
+ - **Atomic Transactions**: Full support for ACID-compliant Commit and Rollback operations.
19
+ - **Efficient Storage**: Fixed-size page management with VFS-based caching and Bitmap space optimization.
20
+ - **Type Safety**: Comprehensive TypeScript definitions for a seamless developer experience.
20
21
 
21
22
  ## Installation
22
23
 
@@ -148,10 +149,10 @@ try {
148
149
  ```
149
150
 
150
151
  ### Auto-Transaction
151
- If you omit the `tx` argument when calling methods like `insert`, `update`, or `delete`, Dataply internally **creates an individual transaction automatically**.
152
+ If you omit the `tx` argument, Dataply creates an internal transaction for each operation.
152
153
 
153
- - **Guaranteed Atomicity**: Even single operations are processed within an internal transaction, ensuring they are only finalized on success and rolled back on failure.
154
- - **Performance Note**: For batch processing or multiple related operations, wrapping them in a single explicit transaction is significantly faster than relying on auto-transactions due to reduced I/O overhead.
154
+ - **Security**: Atomicity is guaranteed even for single operations.
155
+ - **Optimization Tip**: For bulk operations, use an **explicit transaction** to significantly reduce I/O overhead and increase performance.
155
156
 
156
157
  ## API Reference
157
158
 
@@ -201,13 +202,13 @@ Cancels all changes made during the transaction and restores the original state.
201
202
  ### GlobalTransaction Class
202
203
 
203
204
  #### `add(tx: Transaction): void`
204
- Adds an individual transaction from a `Dataply` instance to the global transaction.
205
+ Registers a transaction from a Dataply instance to the global unit.
205
206
 
206
207
  #### `async commit(): Promise<void>`
207
- Atomically commits all added transactions using a 2-Phase Commit (2PC) process.
208
+ Executes an atomic commit across all registered transactions via a 2-Phase Commit (2PC) protocol.
208
209
 
209
210
  #### `async rollback(): Promise<void>`
210
- Rolls back all added transactions.
211
+ Rolls back all registered transactions simultaneously.
211
212
 
212
213
  ## Extending Dataply
213
214
 
@@ -331,12 +332,14 @@ As **Dataply** is currently in Alpha, there are several limitations to keep in m
331
332
 
332
333
  ### Q: Why should I use Dataply instead of a simple JSON file?
333
334
 
334
- The core differences between the commonly used JSON file approach and Dataply are as follows:
335
+ While JSON is simple, Dataply is designed for scalable and reliable data management:
335
336
 
336
- 1. **Memory Efficiency**: While JSON requires loading the entire file into memory, Dataply uses a **page-based storage mechanism**, allowing it to handle large-scale data reliably with a constant memory footprint.
337
- 2. **Superior Search Performance**: Unlike JSON, which requires a full scan (O(N)), Dataply ensures extremely fast lookups (O(log N)) regardless of data size using a **B+Tree index**.
338
- 3. **Data Integrity**: In contrast to JSON files that risk corruption during system failures, Dataply protects your data using **WAL (Write-Ahead Logging)** and **Transactions**.
339
- 4. **Concurrency Control**: Using **MVCC (Multi-Version Concurrency Control)** and **page-level locking**, Dataply delivers peak performance even in environments where multiple users are reading and writing simultaneously.
337
+ | Feature | JSON File Approach | Dataply Record Store |
338
+ | :--- | :--- | :--- |
339
+ | **Memory usage** | Loads entire file into RAM | Constant memory via page-based I/O |
340
+ | **Search speed** | Linear scan (O(N)) | B+Tree Index lookups (O(log N)) |
341
+ | **Integrity** | High risk of corruption on crash | Protected by WAL and Transactions |
342
+ | **Concurrency** | Single-user only | Multi-user via MVCC & Locking |
340
343
 
341
344
  ### Q: What can I build with Dataply?
342
345