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.
- package/dist/cjs/index.cjs +467 -48
- package/dist/esm/index.mjs +467 -48
- package/dist/types/SerializeStrategyAsync.d.ts +4 -1
- package/dist/types/SerializeStrategySync.d.ts +2 -1
- package/dist/types/base/BPTree.d.ts +22 -10
- package/dist/types/base/SerializeStrategy.d.ts +8 -4
- package/dist/types/transaction/BPTreeAsyncSnapshotStrategy.d.ts +2 -1
- package/dist/types/transaction/BPTreeAsyncTransaction.d.ts +10 -3
- package/dist/types/transaction/BPTreeSyncSnapshotStrategy.d.ts +2 -1
- package/dist/types/transaction/BPTreeSyncTransaction.d.ts +12 -3
- package/package.json +1 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -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
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
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(
|
|
1629
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1835
|
-
|
|
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)
|
|
1842
|
-
|
|
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
|
-
*
|
|
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
|
|
2689
|
-
|
|
2690
|
-
|
|
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(
|
|
2764
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2970
|
-
|
|
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)
|
|
2977
|
-
|
|
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
|
-
*
|
|
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();
|