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