serializable-bptree 7.0.2 → 7.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.cjs +441 -42
- package/dist/esm/index.mjs +441 -42
- 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 +5 -1
- package/dist/types/transaction/BPTreeSyncSnapshotStrategy.d.ts +2 -1
- package/dist/types/transaction/BPTreeSyncTransaction.d.ts +7 -1
- package/package.json +1 -1
package/dist/esm/index.mjs
CHANGED
|
@@ -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
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
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(
|
|
1593
|
-
|
|
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,11 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
|
|
|
1611
1652
|
dirtyIds;
|
|
1612
1653
|
createdInTx;
|
|
1613
1654
|
deletedIds;
|
|
1655
|
+
originalNodes = /* @__PURE__ */ new Map();
|
|
1614
1656
|
initialRootId;
|
|
1615
1657
|
transactionRootId;
|
|
1658
|
+
transactionId;
|
|
1659
|
+
initialLastCommittedTransactionId = 0;
|
|
1616
1660
|
constructor(baseTree) {
|
|
1617
1661
|
super(baseTree.strategy, baseTree.comparator, baseTree.option);
|
|
1618
1662
|
this.realBaseTree = baseTree;
|
|
@@ -1623,6 +1667,7 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
|
|
|
1623
1667
|
this.dirtyIds = /* @__PURE__ */ new Set();
|
|
1624
1668
|
this.createdInTx = /* @__PURE__ */ new Set();
|
|
1625
1669
|
this.deletedIds = /* @__PURE__ */ new Set();
|
|
1670
|
+
this.transactionId = Date.now() + Math.random();
|
|
1626
1671
|
}
|
|
1627
1672
|
/**
|
|
1628
1673
|
* Initializes the transaction by capturing the current state of the tree.
|
|
@@ -1639,6 +1684,7 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
|
|
|
1639
1684
|
const root = this._createNode(true, [], [], true);
|
|
1640
1685
|
this.initialRootId = root.id;
|
|
1641
1686
|
}
|
|
1687
|
+
this.initialLastCommittedTransactionId = this.realBaseStrategy.getLastCommittedTransactionId();
|
|
1642
1688
|
this.transactionRootId = this.initialRootId;
|
|
1643
1689
|
this.rootId = this.transactionRootId;
|
|
1644
1690
|
const snapshotStrategy = new BPTreeSyncSnapshotStrategy(this.realBaseStrategy, this.initialRootId);
|
|
@@ -1647,6 +1693,7 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
|
|
|
1647
1693
|
this.dirtyIds.clear();
|
|
1648
1694
|
this.createdInTx.clear();
|
|
1649
1695
|
this.deletedIds.clear();
|
|
1696
|
+
this.realBaseTree.registerTransaction(this.transactionId);
|
|
1650
1697
|
}
|
|
1651
1698
|
getNode(id) {
|
|
1652
1699
|
if (this.txNodes.has(id)) {
|
|
@@ -1655,7 +1702,13 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
|
|
|
1655
1702
|
if (this.deletedIds.has(id)) {
|
|
1656
1703
|
throw new Error(`The tree attempted to reference deleted node '${id}'`);
|
|
1657
1704
|
}
|
|
1658
|
-
|
|
1705
|
+
let baseNode = this.realBaseTree.getObsoleteNode(id);
|
|
1706
|
+
if (!baseNode) {
|
|
1707
|
+
baseNode = this.realBaseStrategy.read(id);
|
|
1708
|
+
}
|
|
1709
|
+
if (!this.originalNodes.has(id) && !this.createdInTx.has(id)) {
|
|
1710
|
+
this.originalNodes.set(id, JSON.parse(JSON.stringify(baseNode)));
|
|
1711
|
+
}
|
|
1659
1712
|
const clone = JSON.parse(JSON.stringify(baseNode));
|
|
1660
1713
|
this.txNodes.set(id, clone);
|
|
1661
1714
|
return clone;
|
|
@@ -1745,9 +1798,10 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
|
|
|
1745
1798
|
* Attempts to commit the transaction.
|
|
1746
1799
|
* Uses Optimistic Locking (Compare-And-Swap) on the root node ID to detect conflicts.
|
|
1747
1800
|
*
|
|
1801
|
+
* @param cleanup Whether to clean up obsolete nodes after commit. Defaults to true.
|
|
1748
1802
|
* @returns The transaction result.
|
|
1749
1803
|
*/
|
|
1750
|
-
commit() {
|
|
1804
|
+
commit(cleanup = true) {
|
|
1751
1805
|
const idMapping = /* @__PURE__ */ new Map();
|
|
1752
1806
|
const finalNodes = [];
|
|
1753
1807
|
for (const oldId of this.dirtyIds) {
|
|
@@ -1795,24 +1849,46 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
|
|
|
1795
1849
|
if (idMapping.has(this.rootId)) {
|
|
1796
1850
|
newRootId = idMapping.get(this.rootId);
|
|
1797
1851
|
}
|
|
1798
|
-
|
|
1799
|
-
|
|
1852
|
+
let success = false;
|
|
1853
|
+
if (finalNodes.length === 0) {
|
|
1854
|
+
success = true;
|
|
1855
|
+
} else if (this.realBaseStrategy.getLastCommittedTransactionId() === this.initialLastCommittedTransactionId) {
|
|
1856
|
+
for (const node of finalNodes) {
|
|
1857
|
+
this.realBaseStrategy.write(node.id, node);
|
|
1858
|
+
}
|
|
1859
|
+
this.realBaseStrategy.compareAndSwapHead(newRootId, this.transactionId);
|
|
1860
|
+
success = true;
|
|
1800
1861
|
}
|
|
1801
|
-
const success = this.realBaseStrategy.compareAndSwapHead(this.initialRootId, newRootId);
|
|
1802
1862
|
if (success) {
|
|
1803
1863
|
const distinctObsolete = /* @__PURE__ */ new Set();
|
|
1804
1864
|
for (const oldId of this.dirtyIds) {
|
|
1805
|
-
if (
|
|
1865
|
+
if (this.createdInTx.has(oldId)) {
|
|
1866
|
+
continue;
|
|
1867
|
+
}
|
|
1868
|
+
if (this.txNodes.has(oldId) || this.deletedIds.has(oldId)) {
|
|
1806
1869
|
distinctObsolete.add(oldId);
|
|
1807
1870
|
}
|
|
1808
1871
|
}
|
|
1872
|
+
if (cleanup) {
|
|
1873
|
+
for (const obsoleteId of distinctObsolete) {
|
|
1874
|
+
if (this.originalNodes.has(obsoleteId)) {
|
|
1875
|
+
this.realBaseTree.addObsoleteNode(
|
|
1876
|
+
this.originalNodes.get(obsoleteId),
|
|
1877
|
+
this.transactionId
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
this.realBaseStrategy.delete(obsoleteId);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
this.realBaseTree.unregisterTransaction(this.transactionId);
|
|
1884
|
+
this.realBaseTree.pruneObsoleteNodes();
|
|
1809
1885
|
return {
|
|
1810
1886
|
success: true,
|
|
1811
1887
|
createdIds: newCreatedIds,
|
|
1812
1888
|
obsoleteIds: Array.from(distinctObsolete)
|
|
1813
1889
|
};
|
|
1814
1890
|
} else {
|
|
1815
|
-
this.rollback();
|
|
1891
|
+
this.rollback(cleanup);
|
|
1816
1892
|
return {
|
|
1817
1893
|
success: false,
|
|
1818
1894
|
createdIds: newCreatedIds,
|
|
@@ -1836,8 +1912,16 @@ var BPTreeSyncTransaction = class extends BPTreeSyncBase {
|
|
|
1836
1912
|
this.realBaseStrategy.delete(id);
|
|
1837
1913
|
}
|
|
1838
1914
|
}
|
|
1915
|
+
this.realBaseTree.unregisterTransaction(this.transactionId);
|
|
1916
|
+
this.realBaseTree.pruneObsoleteNodes();
|
|
1839
1917
|
return createdIds;
|
|
1840
1918
|
}
|
|
1919
|
+
readLock(fn) {
|
|
1920
|
+
return fn();
|
|
1921
|
+
}
|
|
1922
|
+
writeLock(fn) {
|
|
1923
|
+
return fn();
|
|
1924
|
+
}
|
|
1841
1925
|
// Override to do nothing, as transaction handles its own commits
|
|
1842
1926
|
commitHeadBuffer() {
|
|
1843
1927
|
}
|
|
@@ -2475,7 +2559,9 @@ var BPTreeAsyncBase = class extends BPTree {
|
|
|
2475
2559
|
this._nodeUpdateBuffer.clear();
|
|
2476
2560
|
}
|
|
2477
2561
|
async commitNodeDeleteBuffer() {
|
|
2562
|
+
const obsoleteAt = this.getNextTransactionId();
|
|
2478
2563
|
for (const node of this._nodeDeleteBuffer.values()) {
|
|
2564
|
+
this.addObsoleteNode(node, obsoleteAt);
|
|
2479
2565
|
await this.strategy.delete(node.id);
|
|
2480
2566
|
this.nodes.delete(node.id);
|
|
2481
2567
|
}
|
|
@@ -2640,8 +2726,276 @@ var BPTreeAsyncBase = class extends BPTree {
|
|
|
2640
2726
|
}
|
|
2641
2727
|
};
|
|
2642
2728
|
|
|
2729
|
+
// node_modules/ryoiki/dist/esm/index.mjs
|
|
2730
|
+
var Ryoiki = class _Ryoiki {
|
|
2731
|
+
readings;
|
|
2732
|
+
writings;
|
|
2733
|
+
readQueue;
|
|
2734
|
+
writeQueue;
|
|
2735
|
+
static async CatchError(promise) {
|
|
2736
|
+
return await promise.then((v) => [void 0, v]).catch((err) => [err]);
|
|
2737
|
+
}
|
|
2738
|
+
static IsRangeOverlap(a, b) {
|
|
2739
|
+
const [start1, end1] = a;
|
|
2740
|
+
const [start2, end2] = b;
|
|
2741
|
+
if (end1 <= start2 || end2 <= start1) {
|
|
2742
|
+
return false;
|
|
2743
|
+
}
|
|
2744
|
+
return true;
|
|
2745
|
+
}
|
|
2746
|
+
static ERR_ALREADY_EXISTS(lockId) {
|
|
2747
|
+
return new Error(`The '${lockId}' task already existing in queue or running.`);
|
|
2748
|
+
}
|
|
2749
|
+
static ERR_NOT_EXISTS(lockId) {
|
|
2750
|
+
return new Error(`The '${lockId}' task not existing in task queue.`);
|
|
2751
|
+
}
|
|
2752
|
+
static ERR_TIMEOUT(lockId, timeout) {
|
|
2753
|
+
return new Error(`The task with ID '${lockId}' failed to acquire the lock within the timeout(${timeout}ms).`);
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* Constructs a new instance of the Ryoiki class.
|
|
2757
|
+
*/
|
|
2758
|
+
constructor() {
|
|
2759
|
+
this.readings = /* @__PURE__ */ new Map();
|
|
2760
|
+
this.writings = /* @__PURE__ */ new Map();
|
|
2761
|
+
this.readQueue = /* @__PURE__ */ new Map();
|
|
2762
|
+
this.writeQueue = /* @__PURE__ */ new Map();
|
|
2763
|
+
}
|
|
2764
|
+
/**
|
|
2765
|
+
* Creates a range based on a start value and length.
|
|
2766
|
+
* @param start - The starting value of the range.
|
|
2767
|
+
* @param length - The length of the range.
|
|
2768
|
+
* @returns A range tuple [start, start + length].
|
|
2769
|
+
*/
|
|
2770
|
+
range(start, length) {
|
|
2771
|
+
return [start, start + length];
|
|
2772
|
+
}
|
|
2773
|
+
rangeOverlapping(tasks, range) {
|
|
2774
|
+
return Array.from(tasks.values()).some((t) => _Ryoiki.IsRangeOverlap(t.range, range));
|
|
2775
|
+
}
|
|
2776
|
+
isSameRange(a, b) {
|
|
2777
|
+
const [a1, a2] = a;
|
|
2778
|
+
const [b1, b2] = b;
|
|
2779
|
+
return a1 === b1 && a2 === b2;
|
|
2780
|
+
}
|
|
2781
|
+
fetchUnitAndRun(queue, workspaces) {
|
|
2782
|
+
for (const [id, unit] of queue) {
|
|
2783
|
+
if (!unit.condition()) {
|
|
2784
|
+
continue;
|
|
2785
|
+
}
|
|
2786
|
+
this._alloc(queue, workspaces, id);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
_handleOverload(args, handlers, argPatterns) {
|
|
2790
|
+
for (const [key, pattern] of Object.entries(argPatterns)) {
|
|
2791
|
+
if (this._matchArgs(args, pattern)) {
|
|
2792
|
+
return handlers[key](...args);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
throw new Error("Invalid arguments");
|
|
2796
|
+
}
|
|
2797
|
+
_matchArgs(args, pattern) {
|
|
2798
|
+
return args.every((arg, index) => {
|
|
2799
|
+
const expectedType = pattern[index];
|
|
2800
|
+
if (expectedType === void 0) return typeof arg === "undefined";
|
|
2801
|
+
if (expectedType === Function) return typeof arg === "function";
|
|
2802
|
+
if (expectedType === Number) return typeof arg === "number";
|
|
2803
|
+
if (expectedType === Array) return Array.isArray(arg);
|
|
2804
|
+
return false;
|
|
2805
|
+
});
|
|
2806
|
+
}
|
|
2807
|
+
_createRandomId() {
|
|
2808
|
+
const timestamp = Date.now().toString(36);
|
|
2809
|
+
const random = Math.random().toString(36).substring(2);
|
|
2810
|
+
return `${timestamp}${random}`;
|
|
2811
|
+
}
|
|
2812
|
+
_alloc(queue, workspaces, lockId) {
|
|
2813
|
+
const unit = queue.get(lockId);
|
|
2814
|
+
if (!unit) {
|
|
2815
|
+
throw _Ryoiki.ERR_NOT_EXISTS(lockId);
|
|
2816
|
+
}
|
|
2817
|
+
workspaces.set(lockId, unit);
|
|
2818
|
+
queue.delete(lockId);
|
|
2819
|
+
unit.alloc();
|
|
2820
|
+
}
|
|
2821
|
+
_free(workspaces, lockId) {
|
|
2822
|
+
const unit = workspaces.get(lockId);
|
|
2823
|
+
if (!unit) {
|
|
2824
|
+
throw _Ryoiki.ERR_NOT_EXISTS(lockId);
|
|
2825
|
+
}
|
|
2826
|
+
workspaces.delete(lockId);
|
|
2827
|
+
unit.free();
|
|
2828
|
+
}
|
|
2829
|
+
_lock(queue, range, timeout, task, condition) {
|
|
2830
|
+
return new Promise((resolve, reject) => {
|
|
2831
|
+
let timeoutId = null;
|
|
2832
|
+
if (timeout >= 0) {
|
|
2833
|
+
timeoutId = setTimeout(() => {
|
|
2834
|
+
reject(_Ryoiki.ERR_TIMEOUT(id, timeout));
|
|
2835
|
+
}, timeout);
|
|
2836
|
+
}
|
|
2837
|
+
const id = this._createRandomId();
|
|
2838
|
+
const alloc = async () => {
|
|
2839
|
+
if (timeoutId !== null) {
|
|
2840
|
+
clearTimeout(timeoutId);
|
|
2841
|
+
}
|
|
2842
|
+
const [err, v] = await _Ryoiki.CatchError(task(id));
|
|
2843
|
+
if (err) reject(err);
|
|
2844
|
+
else resolve(v);
|
|
2845
|
+
};
|
|
2846
|
+
const fetch = () => {
|
|
2847
|
+
this.fetchUnitAndRun(this.readQueue, this.readings);
|
|
2848
|
+
this.fetchUnitAndRun(this.writeQueue, this.writings);
|
|
2849
|
+
};
|
|
2850
|
+
queue.set(id, { id, range, condition, alloc, free: fetch });
|
|
2851
|
+
fetch();
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2854
|
+
_checkWorking(range, workspaces) {
|
|
2855
|
+
let isLocked = false;
|
|
2856
|
+
for (const lock of workspaces.values()) {
|
|
2857
|
+
if (_Ryoiki.IsRangeOverlap(range, lock.range)) {
|
|
2858
|
+
isLocked = true;
|
|
2859
|
+
break;
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
return isLocked;
|
|
2863
|
+
}
|
|
2864
|
+
/**
|
|
2865
|
+
* Checks if there is any active read lock within the specified range.
|
|
2866
|
+
* @param range The range to check for active read locks.
|
|
2867
|
+
* @returns `true` if there is an active read lock within the range, `false` otherwise.
|
|
2868
|
+
*/
|
|
2869
|
+
isReading(range) {
|
|
2870
|
+
return this._checkWorking(range, this.readings);
|
|
2871
|
+
}
|
|
2872
|
+
/**
|
|
2873
|
+
* Checks if there is any active write lock within the specified range.
|
|
2874
|
+
* @param range The range to check for active write locks.
|
|
2875
|
+
* @returns `true` if there is an active write lock within the range, `false` otherwise.
|
|
2876
|
+
*/
|
|
2877
|
+
isWriting(range) {
|
|
2878
|
+
return this._checkWorking(range, this.writings);
|
|
2879
|
+
}
|
|
2880
|
+
/**
|
|
2881
|
+
* Checks if a read lock can be acquired within the specified range.
|
|
2882
|
+
* @param range The range to check for read lock availability.
|
|
2883
|
+
* @returns `true` if a read lock can be acquired, `false` otherwise.
|
|
2884
|
+
*/
|
|
2885
|
+
canRead(range) {
|
|
2886
|
+
const writing = this.isWriting(range);
|
|
2887
|
+
return !writing;
|
|
2888
|
+
}
|
|
2889
|
+
/**
|
|
2890
|
+
* Checks if a write lock can be acquired within the specified range.
|
|
2891
|
+
* @param range The range to check for write lock availability.
|
|
2892
|
+
* @returns `true` if a write lock can be acquired, `false` otherwise.
|
|
2893
|
+
*/
|
|
2894
|
+
canWrite(range) {
|
|
2895
|
+
const reading = this.isReading(range);
|
|
2896
|
+
const writing = this.isWriting(range);
|
|
2897
|
+
return !reading && !writing;
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* Internal implementation of the read lock. Handles both overloads.
|
|
2901
|
+
* @template T - The return type of the task.
|
|
2902
|
+
* @param arg0 - Either a range or a task callback.
|
|
2903
|
+
* If a range is provided, the task is the second argument.
|
|
2904
|
+
* @param arg1 - The task to execute, required if a range is provided.
|
|
2905
|
+
* @param arg2 - The timeout for acquiring the lock.
|
|
2906
|
+
* If the lock cannot be acquired within this period, an error will be thrown.
|
|
2907
|
+
* If this value is not provided, no timeout will be set.
|
|
2908
|
+
* @returns A promise resolving to the result of the task execution.
|
|
2909
|
+
*/
|
|
2910
|
+
readLock(arg0, arg1, arg2) {
|
|
2911
|
+
const [range, task, timeout] = this._handleOverload(
|
|
2912
|
+
[arg0, arg1, arg2],
|
|
2913
|
+
{
|
|
2914
|
+
rangeTask: (range2, task2) => [range2, task2, -1],
|
|
2915
|
+
rangeTaskTimeout: (range2, task2, timeout2) => [range2, task2, timeout2],
|
|
2916
|
+
task: (task2) => [[-Infinity, Infinity], task2, -1],
|
|
2917
|
+
taskTimeout: (task2, timeout2) => [[-Infinity, Infinity], task2, timeout2]
|
|
2918
|
+
},
|
|
2919
|
+
{
|
|
2920
|
+
task: [Function],
|
|
2921
|
+
taskTimeout: [Function, Number],
|
|
2922
|
+
rangeTask: [Array, Function],
|
|
2923
|
+
rangeTaskTimeout: [Array, Function, Number]
|
|
2924
|
+
}
|
|
2925
|
+
);
|
|
2926
|
+
return this._lock(
|
|
2927
|
+
this.readQueue,
|
|
2928
|
+
range,
|
|
2929
|
+
timeout,
|
|
2930
|
+
task,
|
|
2931
|
+
() => !this.rangeOverlapping(this.writings, range)
|
|
2932
|
+
);
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Internal implementation of the write lock. Handles both overloads.
|
|
2936
|
+
* @template T - The return type of the task.
|
|
2937
|
+
* @param arg0 - Either a range or a task callback.
|
|
2938
|
+
* If a range is provided, the task is the second argument.
|
|
2939
|
+
* @param arg1 - The task to execute, required if a range is provided.
|
|
2940
|
+
* @param arg2 - The timeout for acquiring the lock.
|
|
2941
|
+
* If the lock cannot be acquired within this period, an error will be thrown.
|
|
2942
|
+
* If this value is not provided, no timeout will be set.
|
|
2943
|
+
* @returns A promise resolving to the result of the task execution.
|
|
2944
|
+
*/
|
|
2945
|
+
writeLock(arg0, arg1, arg2) {
|
|
2946
|
+
const [range, task, timeout] = this._handleOverload(
|
|
2947
|
+
[arg0, arg1, arg2],
|
|
2948
|
+
{
|
|
2949
|
+
rangeTask: (range2, task2) => [range2, task2, -1],
|
|
2950
|
+
rangeTaskTimeout: (range2, task2, timeout2) => [range2, task2, timeout2],
|
|
2951
|
+
task: (task2) => [[-Infinity, Infinity], task2, -1],
|
|
2952
|
+
taskTimeout: (task2, timeout2) => [[-Infinity, Infinity], task2, timeout2]
|
|
2953
|
+
},
|
|
2954
|
+
{
|
|
2955
|
+
task: [Function],
|
|
2956
|
+
taskTimeout: [Function, Number],
|
|
2957
|
+
rangeTask: [Array, Function],
|
|
2958
|
+
rangeTaskTimeout: [Array, Function, Number]
|
|
2959
|
+
}
|
|
2960
|
+
);
|
|
2961
|
+
return this._lock(
|
|
2962
|
+
this.writeQueue,
|
|
2963
|
+
range,
|
|
2964
|
+
timeout,
|
|
2965
|
+
task,
|
|
2966
|
+
() => {
|
|
2967
|
+
return !this.rangeOverlapping(this.writings, range) && !this.rangeOverlapping(this.readings, range);
|
|
2968
|
+
}
|
|
2969
|
+
);
|
|
2970
|
+
}
|
|
2971
|
+
/**
|
|
2972
|
+
* Releases a read lock by its lock ID.
|
|
2973
|
+
* @param lockId - The unique identifier for the lock to release.
|
|
2974
|
+
*/
|
|
2975
|
+
readUnlock(lockId) {
|
|
2976
|
+
this._free(this.readings, lockId);
|
|
2977
|
+
}
|
|
2978
|
+
/**
|
|
2979
|
+
* Releases a write lock by its lock ID.
|
|
2980
|
+
* @param lockId - The unique identifier for the lock to release.
|
|
2981
|
+
*/
|
|
2982
|
+
writeUnlock(lockId) {
|
|
2983
|
+
this._free(this.writings, lockId);
|
|
2984
|
+
}
|
|
2985
|
+
};
|
|
2986
|
+
|
|
2643
2987
|
// src/SerializeStrategyAsync.ts
|
|
2644
2988
|
var SerializeStrategyAsync = class extends SerializeStrategy {
|
|
2989
|
+
lock = new Ryoiki();
|
|
2990
|
+
async acquireLock(action) {
|
|
2991
|
+
let lockId;
|
|
2992
|
+
return this.lock.writeLock((_lockId) => {
|
|
2993
|
+
lockId = _lockId;
|
|
2994
|
+
return action();
|
|
2995
|
+
}).finally(() => {
|
|
2996
|
+
this.lock.writeUnlock(lockId);
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2645
2999
|
async getHeadData(key, defaultValue) {
|
|
2646
3000
|
if (!Object.hasOwn(this.head.data, key)) {
|
|
2647
3001
|
await this.setHeadData(key, defaultValue);
|
|
@@ -2658,13 +3012,13 @@ var SerializeStrategyAsync = class extends SerializeStrategy {
|
|
|
2658
3012
|
await this.setHeadData(key, next);
|
|
2659
3013
|
return current;
|
|
2660
3014
|
}
|
|
2661
|
-
async
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
3015
|
+
async getLastCommittedTransactionId() {
|
|
3016
|
+
return this.lastCommittedTransactionId;
|
|
3017
|
+
}
|
|
3018
|
+
async compareAndSwapHead(newRoot, newTxId) {
|
|
2665
3019
|
this.head.root = newRoot;
|
|
3020
|
+
this.lastCommittedTransactionId = newTxId;
|
|
2666
3021
|
await this.writeHead(this.head);
|
|
2667
|
-
return true;
|
|
2668
3022
|
}
|
|
2669
3023
|
};
|
|
2670
3024
|
var InMemoryStoreStrategyAsync = class extends SerializeStrategyAsync {
|
|
@@ -2733,8 +3087,11 @@ var BPTreeAsyncSnapshotStrategy = class extends SerializeStrategyAsync {
|
|
|
2733
3087
|
this.snapshotHead.root = head.root;
|
|
2734
3088
|
this.snapshotHead.data = { ...head.data };
|
|
2735
3089
|
}
|
|
2736
|
-
async compareAndSwapHead(
|
|
2737
|
-
|
|
3090
|
+
async compareAndSwapHead(newRoot, newTxId) {
|
|
3091
|
+
await this.baseStrategy.compareAndSwapHead(newRoot, newTxId);
|
|
3092
|
+
}
|
|
3093
|
+
async getLastCommittedTransactionId() {
|
|
3094
|
+
return await this.baseStrategy.getLastCommittedTransactionId();
|
|
2738
3095
|
}
|
|
2739
3096
|
async getHeadData(key, defaultValue) {
|
|
2740
3097
|
return this.snapshotHead.data[key] ?? defaultValue;
|
|
@@ -2755,8 +3112,11 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
|
|
|
2755
3112
|
dirtyIds;
|
|
2756
3113
|
createdInTx;
|
|
2757
3114
|
deletedIds;
|
|
3115
|
+
originalNodes = /* @__PURE__ */ new Map();
|
|
2758
3116
|
initialRootId;
|
|
2759
3117
|
transactionRootId;
|
|
3118
|
+
transactionId;
|
|
3119
|
+
initialLastCommittedTransactionId = 0;
|
|
2760
3120
|
constructor(baseTree) {
|
|
2761
3121
|
super(baseTree.strategy, baseTree.comparator, baseTree.option);
|
|
2762
3122
|
this.realBaseTree = baseTree;
|
|
@@ -2767,6 +3127,7 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
|
|
|
2767
3127
|
this.dirtyIds = /* @__PURE__ */ new Set();
|
|
2768
3128
|
this.createdInTx = /* @__PURE__ */ new Set();
|
|
2769
3129
|
this.deletedIds = /* @__PURE__ */ new Set();
|
|
3130
|
+
this.transactionId = Date.now() + Math.random();
|
|
2770
3131
|
}
|
|
2771
3132
|
/**
|
|
2772
3133
|
* Initializes the transaction by capturing the current state of the tree.
|
|
@@ -2783,6 +3144,7 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
|
|
|
2783
3144
|
const root = await this._createNode(true, [], [], true);
|
|
2784
3145
|
this.initialRootId = root.id;
|
|
2785
3146
|
}
|
|
3147
|
+
this.initialLastCommittedTransactionId = await this.realBaseStrategy.getLastCommittedTransactionId();
|
|
2786
3148
|
this.transactionRootId = this.initialRootId;
|
|
2787
3149
|
this.rootId = this.transactionRootId;
|
|
2788
3150
|
const snapshotStrategy = new BPTreeAsyncSnapshotStrategy(this.realBaseStrategy, this.initialRootId);
|
|
@@ -2791,6 +3153,7 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
|
|
|
2791
3153
|
this.dirtyIds.clear();
|
|
2792
3154
|
this.createdInTx.clear();
|
|
2793
3155
|
this.deletedIds.clear();
|
|
3156
|
+
this.realBaseTree.registerTransaction(this.transactionId);
|
|
2794
3157
|
}
|
|
2795
3158
|
async getNode(id) {
|
|
2796
3159
|
if (this.txNodes.has(id)) {
|
|
@@ -2799,7 +3162,13 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
|
|
|
2799
3162
|
if (this.deletedIds.has(id)) {
|
|
2800
3163
|
throw new Error(`The tree attempted to reference deleted node '${id}'`);
|
|
2801
3164
|
}
|
|
2802
|
-
|
|
3165
|
+
let baseNode = this.realBaseTree.getObsoleteNode(id);
|
|
3166
|
+
if (!baseNode) {
|
|
3167
|
+
baseNode = await this.realBaseStrategy.read(id);
|
|
3168
|
+
}
|
|
3169
|
+
if (!this.originalNodes.has(id) && !this.createdInTx.has(id)) {
|
|
3170
|
+
this.originalNodes.set(id, JSON.parse(JSON.stringify(baseNode)));
|
|
3171
|
+
}
|
|
2803
3172
|
const clone = JSON.parse(JSON.stringify(baseNode));
|
|
2804
3173
|
this.txNodes.set(id, clone);
|
|
2805
3174
|
return clone;
|
|
@@ -2889,9 +3258,10 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
|
|
|
2889
3258
|
* Attempts to commit the transaction.
|
|
2890
3259
|
* Uses Optimistic Locking (Compare-And-Swap) on the root node ID to detect conflicts.
|
|
2891
3260
|
*
|
|
3261
|
+
* @param cleanup Whether to clean up obsolete nodes after commit. Defaults to true.
|
|
2892
3262
|
* @returns A promise that resolves to the transaction result.
|
|
2893
3263
|
*/
|
|
2894
|
-
async commit() {
|
|
3264
|
+
async commit(cleanup = true) {
|
|
2895
3265
|
const idMapping = /* @__PURE__ */ new Map();
|
|
2896
3266
|
const finalNodes = [];
|
|
2897
3267
|
for (const oldId of this.dirtyIds) {
|
|
@@ -2939,24 +3309,51 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
|
|
|
2939
3309
|
if (idMapping.has(this.rootId)) {
|
|
2940
3310
|
newRootId = idMapping.get(this.rootId);
|
|
2941
3311
|
}
|
|
2942
|
-
|
|
2943
|
-
|
|
3312
|
+
let success = false;
|
|
3313
|
+
if (finalNodes.length === 0) {
|
|
3314
|
+
success = true;
|
|
3315
|
+
} else {
|
|
3316
|
+
success = await this.realBaseStrategy.acquireLock(async () => {
|
|
3317
|
+
if (await this.realBaseStrategy.getLastCommittedTransactionId() === this.initialLastCommittedTransactionId) {
|
|
3318
|
+
for (const node of finalNodes) {
|
|
3319
|
+
await this.realBaseStrategy.write(node.id, node);
|
|
3320
|
+
}
|
|
3321
|
+
await this.realBaseStrategy.compareAndSwapHead(newRootId, this.transactionId);
|
|
3322
|
+
return true;
|
|
3323
|
+
}
|
|
3324
|
+
return false;
|
|
3325
|
+
});
|
|
2944
3326
|
}
|
|
2945
|
-
const success = await this.realBaseStrategy.compareAndSwapHead(this.initialRootId, newRootId);
|
|
2946
3327
|
if (success) {
|
|
2947
3328
|
const distinctObsolete = /* @__PURE__ */ new Set();
|
|
2948
3329
|
for (const oldId of this.dirtyIds) {
|
|
2949
|
-
if (
|
|
3330
|
+
if (this.createdInTx.has(oldId)) {
|
|
3331
|
+
continue;
|
|
3332
|
+
}
|
|
3333
|
+
if (this.txNodes.has(oldId) || this.deletedIds.has(oldId)) {
|
|
2950
3334
|
distinctObsolete.add(oldId);
|
|
2951
3335
|
}
|
|
2952
3336
|
}
|
|
3337
|
+
if (cleanup) {
|
|
3338
|
+
for (const obsoleteId of distinctObsolete) {
|
|
3339
|
+
if (this.originalNodes.has(obsoleteId)) {
|
|
3340
|
+
this.realBaseTree.addObsoleteNode(
|
|
3341
|
+
this.originalNodes.get(obsoleteId),
|
|
3342
|
+
this.transactionId
|
|
3343
|
+
);
|
|
3344
|
+
}
|
|
3345
|
+
await this.realBaseStrategy.delete(obsoleteId);
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
this.realBaseTree.unregisterTransaction(this.transactionId);
|
|
3349
|
+
this.realBaseTree.pruneObsoleteNodes();
|
|
2953
3350
|
return {
|
|
2954
3351
|
success: true,
|
|
2955
3352
|
createdIds: newCreatedIds,
|
|
2956
3353
|
obsoleteIds: Array.from(distinctObsolete)
|
|
2957
3354
|
};
|
|
2958
3355
|
} else {
|
|
2959
|
-
await this.rollback();
|
|
3356
|
+
await this.rollback(cleanup);
|
|
2960
3357
|
return {
|
|
2961
3358
|
success: false,
|
|
2962
3359
|
createdIds: newCreatedIds,
|
|
@@ -2980,6 +3377,8 @@ var BPTreeAsyncTransaction = class extends BPTreeAsyncBase {
|
|
|
2980
3377
|
await this.realBaseStrategy.delete(id);
|
|
2981
3378
|
}
|
|
2982
3379
|
}
|
|
3380
|
+
this.realBaseTree.unregisterTransaction(this.transactionId);
|
|
3381
|
+
this.realBaseTree.pruneObsoleteNodes();
|
|
2983
3382
|
return createdIds;
|
|
2984
3383
|
}
|
|
2985
3384
|
async readLock(fn) {
|