dataply 0.0.24-alpha.5 → 0.0.24-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/index.js CHANGED
@@ -9465,8 +9465,6 @@ var Transaction = class {
9465
9465
  pageLocks = /* @__PURE__ */ new Map();
9466
9466
  /** Dirty Pages modified by the transaction: PageID -> Modified Page Buffer */
9467
9467
  dirtyPages = /* @__PURE__ */ new Map();
9468
- /** Promise chain for serializing operations on this transaction */
9469
- serialPromise = Promise.resolve();
9470
9468
  /** Undo pages: PageID -> Original Page Buffer (Snapshot) */
9471
9469
  undoPages = /* @__PURE__ */ new Map();
9472
9470
  /** BPTree Transaction instance */
@@ -9477,6 +9475,8 @@ var Transaction = class {
9477
9475
  commitHooks = [];
9478
9476
  /** Page MVCC Strategy for disk access */
9479
9477
  pageStrategy;
9478
+ /** Release function for global write lock, set by DataplyAPI */
9479
+ _writeLockRelease = null;
9480
9480
  /**
9481
9481
  * Sets the BPTree transaction.
9482
9482
  * @param tx BPTree transaction
@@ -9508,28 +9508,21 @@ var Transaction = class {
9508
9508
  * Registers a commit hook.
9509
9509
  * @param hook Function to execute
9510
9510
  */
9511
+ onCommit(hook) {
9512
+ this.commitHooks.push(hook);
9513
+ }
9511
9514
  /**
9512
- * Serializes asynchronous operations on this transaction.
9513
- * Ensures that concurrent calls (e.g., insertBatch + commit) are queued and executed sequentially.
9514
- * @param fn The async function to serialize
9515
- * @returns The result of the function
9515
+ * Sets the global write lock release function.
9516
+ * Called by DataplyAPI.runWithDefaultWrite when acquiring the lock.
9516
9517
  */
9517
- async serialize(fn) {
9518
- const previous = this.serialPromise;
9519
- let resolveLock;
9520
- this.serialPromise = new Promise((resolve) => {
9521
- resolveLock = resolve;
9522
- });
9523
- return previous.then(async () => {
9524
- try {
9525
- return await fn();
9526
- } finally {
9527
- resolveLock();
9528
- }
9529
- });
9518
+ __setWriteLockRelease(release) {
9519
+ this._writeLockRelease = release;
9530
9520
  }
9531
- onCommit(hook) {
9532
- this.commitHooks.push(hook);
9521
+ /**
9522
+ * Returns whether this transaction already has a write lock.
9523
+ */
9524
+ __hasWriteLockRelease() {
9525
+ return this._writeLockRelease !== null;
9533
9526
  }
9534
9527
  /**
9535
9528
  * Reads a page. Uses dirty buffer if available, otherwise disk.
@@ -9576,7 +9569,7 @@ var Transaction = class {
9576
9569
  * Commits the transaction.
9577
9570
  */
9578
9571
  async commit() {
9579
- return this.serialize(async () => {
9572
+ try {
9580
9573
  await this.context.run(this, async () => {
9581
9574
  for (const hook of this.commitHooks) {
9582
9575
  await hook();
@@ -9604,20 +9597,30 @@ var Transaction = class {
9604
9597
  this.dirtyPages.clear();
9605
9598
  this.undoPages.clear();
9606
9599
  this.releaseAllLocks();
9607
- });
9600
+ } finally {
9601
+ if (this._writeLockRelease) {
9602
+ this._writeLockRelease();
9603
+ this._writeLockRelease = null;
9604
+ }
9605
+ }
9608
9606
  }
9609
9607
  /**
9610
9608
  * Rolls back the transaction.
9611
9609
  */
9612
9610
  async rollback() {
9613
- return this.serialize(async () => {
9611
+ try {
9614
9612
  if (this.bptreeTx) {
9615
9613
  this.bptreeTx.rollback();
9616
9614
  }
9617
9615
  this.dirtyPages.clear();
9618
9616
  this.undoPages.clear();
9619
9617
  this.releaseAllLocks();
9620
- });
9618
+ } finally {
9619
+ if (this._writeLockRelease) {
9620
+ this._writeLockRelease();
9621
+ this._writeLockRelease = null;
9622
+ }
9623
+ }
9621
9624
  }
9622
9625
  /**
9623
9626
  * Returns the dirty pages map.
@@ -9698,6 +9701,8 @@ var DataplyAPI = class {
9698
9701
  /** Whether the database was created this time. */
9699
9702
  isNewlyCreated;
9700
9703
  txIdCounter;
9704
+ /** Promise-chain mutex for serializing write operations */
9705
+ writeQueue = Promise.resolve();
9701
9706
  /**
9702
9707
  * Verifies if the page file is a valid Dataply file.
9703
9708
  * The metadata page must be located at the beginning of the Dataply file.
@@ -9855,13 +9860,58 @@ var DataplyAPI = class {
9855
9860
  * @param tx The transaction to use. If not provided, a new transaction is created.
9856
9861
  * @returns The result of the callback function.
9857
9862
  */
9863
+ /**
9864
+ * Acquires the global write lock.
9865
+ * Returns a release function that MUST be called to unlock.
9866
+ * Used internally by runWithDefaultWrite.
9867
+ * @returns A release function
9868
+ */
9869
+ acquireWriteLock() {
9870
+ const previous = this.writeQueue;
9871
+ let release;
9872
+ this.writeQueue = new Promise((resolve) => {
9873
+ release = resolve;
9874
+ });
9875
+ return previous.then(() => release);
9876
+ }
9877
+ /**
9878
+ * Runs a write callback within a transaction context with global write serialization.
9879
+ * If no transaction is provided, a new transaction is created, committed on success, rolled back on error.
9880
+ * If a transaction is provided (external), the write lock is acquired on first call and held until commit/rollback.
9881
+ * Subclasses MUST use this method for all write operations instead of runWithDefault.
9882
+ * @param callback The callback function to run.
9883
+ * @param tx Optional external transaction.
9884
+ * @returns The result of the callback.
9885
+ */
9886
+ async runWithDefaultWrite(callback, tx) {
9887
+ if (!tx) {
9888
+ const release = await this.acquireWriteLock();
9889
+ const internalTx = this.createTransaction();
9890
+ internalTx.__setWriteLockRelease(release);
9891
+ const [error2, result2] = await catchPromise(this.txContext.run(internalTx, () => callback(internalTx)));
9892
+ if (error2) {
9893
+ await internalTx.rollback();
9894
+ throw error2;
9895
+ }
9896
+ await internalTx.commit();
9897
+ return result2;
9898
+ }
9899
+ if (!tx.__hasWriteLockRelease()) {
9900
+ const release = await this.acquireWriteLock();
9901
+ tx.__setWriteLockRelease(release);
9902
+ }
9903
+ const [error, result] = await catchPromise(this.txContext.run(tx, () => callback(tx)));
9904
+ if (error) {
9905
+ throw error;
9906
+ }
9907
+ return result;
9908
+ }
9858
9909
  async runWithDefault(callback, tx) {
9859
9910
  const isInternalTx = !tx;
9860
9911
  if (!tx) {
9861
9912
  tx = this.createTransaction();
9862
9913
  }
9863
- const run = () => catchPromise(this.txContext.run(tx, () => callback(tx)));
9864
- const [error, result] = isInternalTx ? await run() : await tx.serialize(run);
9914
+ const [error, result] = await catchPromise(this.txContext.run(tx, () => callback(tx)));
9865
9915
  if (error) {
9866
9916
  if (isInternalTx) {
9867
9917
  await tx.rollback();
@@ -9889,20 +9939,6 @@ var DataplyAPI = class {
9889
9939
  tx = this.createTransaction();
9890
9940
  }
9891
9941
  let hasError = false;
9892
- if (!isInternalTx) {
9893
- const values = await tx.serialize(async () => {
9894
- const collected = [];
9895
- const generator = this.txContext.stream(tx, () => callback(tx));
9896
- for await (const value of generator) {
9897
- collected.push(value);
9898
- }
9899
- return collected;
9900
- });
9901
- for (const value of values) {
9902
- yield value;
9903
- }
9904
- return;
9905
- }
9906
9942
  try {
9907
9943
  const generator = this.txContext.stream(tx, () => callback(tx));
9908
9944
  for await (const value of generator) {
@@ -9910,10 +9946,12 @@ var DataplyAPI = class {
9910
9946
  }
9911
9947
  } catch (error) {
9912
9948
  hasError = true;
9913
- await tx.rollback();
9949
+ if (isInternalTx) {
9950
+ await tx.rollback();
9951
+ }
9914
9952
  throw error;
9915
9953
  } finally {
9916
- if (!hasError) {
9954
+ if (!hasError && isInternalTx) {
9917
9955
  await tx.commit();
9918
9956
  }
9919
9957
  }
@@ -9939,7 +9977,7 @@ var DataplyAPI = class {
9939
9977
  if (!this.initialized) {
9940
9978
  throw new Error("Dataply instance is not initialized");
9941
9979
  }
9942
- return this.runWithDefault(async (tx2) => {
9980
+ return this.runWithDefaultWrite(async (tx2) => {
9943
9981
  incrementRowCount = incrementRowCount ?? true;
9944
9982
  if (typeof data === "string") {
9945
9983
  data = this.textCodec.encode(data);
@@ -9959,7 +9997,7 @@ var DataplyAPI = class {
9959
9997
  if (!this.initialized) {
9960
9998
  throw new Error("Dataply instance is not initialized");
9961
9999
  }
9962
- return this.runWithDefault(async (tx2) => {
10000
+ return this.runWithDefaultWrite(async (tx2) => {
9963
10001
  incrementRowCount = incrementRowCount ?? true;
9964
10002
  if (typeof data === "string") {
9965
10003
  data = this.textCodec.encode(data);
@@ -9980,7 +10018,7 @@ var DataplyAPI = class {
9980
10018
  if (!this.initialized) {
9981
10019
  throw new Error("Dataply instance is not initialized");
9982
10020
  }
9983
- return this.runWithDefault(async (tx2) => {
10021
+ return this.runWithDefaultWrite(async (tx2) => {
9984
10022
  incrementRowCount = incrementRowCount ?? true;
9985
10023
  const encodedList = dataList.map(
9986
10024
  (data) => typeof data === "string" ? this.textCodec.encode(data) : data
@@ -9998,7 +10036,7 @@ var DataplyAPI = class {
9998
10036
  if (!this.initialized) {
9999
10037
  throw new Error("Dataply instance is not initialized");
10000
10038
  }
10001
- return this.runWithDefault(async (tx2) => {
10039
+ return this.runWithDefaultWrite(async (tx2) => {
10002
10040
  if (typeof data === "string") {
10003
10041
  data = this.textCodec.encode(data);
10004
10042
  }
@@ -10015,7 +10053,7 @@ var DataplyAPI = class {
10015
10053
  if (!this.initialized) {
10016
10054
  throw new Error("Dataply instance is not initialized");
10017
10055
  }
10018
- return this.runWithDefault(async (tx2) => {
10056
+ return this.runWithDefaultWrite(async (tx2) => {
10019
10057
  decrementRowCount = decrementRowCount ?? true;
10020
10058
  await this.rowTableEngine.delete(pk, decrementRowCount, tx2);
10021
10059
  }, tx);
@@ -40,6 +40,8 @@ export declare class DataplyAPI {
40
40
  /** Whether the database was created this time. */
41
41
  private readonly isNewlyCreated;
42
42
  private txIdCounter;
43
+ /** Promise-chain mutex for serializing write operations */
44
+ private writeQueue;
43
45
  constructor(file: string, options: DataplyOptions);
44
46
  /**
45
47
  * Verifies if the page file is a valid Dataply file.
@@ -91,6 +93,23 @@ export declare class DataplyAPI {
91
93
  * @param tx The transaction to use. If not provided, a new transaction is created.
92
94
  * @returns The result of the callback function.
93
95
  */
96
+ /**
97
+ * Acquires the global write lock.
98
+ * Returns a release function that MUST be called to unlock.
99
+ * Used internally by runWithDefaultWrite.
100
+ * @returns A release function
101
+ */
102
+ protected acquireWriteLock(): Promise<() => void>;
103
+ /**
104
+ * Runs a write callback within a transaction context with global write serialization.
105
+ * If no transaction is provided, a new transaction is created, committed on success, rolled back on error.
106
+ * If a transaction is provided (external), the write lock is acquired on first call and held until commit/rollback.
107
+ * Subclasses MUST use this method for all write operations instead of runWithDefault.
108
+ * @param callback The callback function to run.
109
+ * @param tx Optional external transaction.
110
+ * @returns The result of the callback.
111
+ */
112
+ protected runWithDefaultWrite<T>(callback: (tx: Transaction) => Promise<T>, tx?: Transaction): Promise<T>;
94
113
  protected runWithDefault<T>(callback: (tx: Transaction) => Promise<T>, tx?: Transaction): Promise<T>;
95
114
  /**
96
115
  * Runs a generator callback function within a transaction context.
@@ -19,8 +19,6 @@ export declare class Transaction {
19
19
  private pageLocks;
20
20
  /** Dirty Pages modified by the transaction: PageID -> Modified Page Buffer */
21
21
  private dirtyPages;
22
- /** Promise chain for serializing operations on this transaction */
23
- private serialPromise;
24
22
  /** Undo pages: PageID -> Original Page Buffer (Snapshot) */
25
23
  private undoPages;
26
24
  /** BPTree Transaction instance */
@@ -31,6 +29,8 @@ export declare class Transaction {
31
29
  private commitHooks;
32
30
  /** Page MVCC Strategy for disk access */
33
31
  private readonly pageStrategy;
32
+ /** Release function for global write lock, set by DataplyAPI */
33
+ private _writeLockRelease;
34
34
  /**
35
35
  * @param id Transaction ID
36
36
  * @param context Transaction context
@@ -62,14 +62,16 @@ export declare class Transaction {
62
62
  * Registers a commit hook.
63
63
  * @param hook Function to execute
64
64
  */
65
+ onCommit(hook: () => Promise<void>): void;
65
66
  /**
66
- * Serializes asynchronous operations on this transaction.
67
- * Ensures that concurrent calls (e.g., insertBatch + commit) are queued and executed sequentially.
68
- * @param fn The async function to serialize
69
- * @returns The result of the function
67
+ * Sets the global write lock release function.
68
+ * Called by DataplyAPI.runWithDefaultWrite when acquiring the lock.
70
69
  */
71
- serialize<T>(fn: () => Promise<T>): Promise<T>;
72
- onCommit(hook: () => Promise<void>): void;
70
+ __setWriteLockRelease(release: () => void): void;
71
+ /**
72
+ * Returns whether this transaction already has a write lock.
73
+ */
74
+ __hasWriteLockRelease(): boolean;
73
75
  /**
74
76
  * Reads a page. Uses dirty buffer if available, otherwise disk.
75
77
  * @param pageId Page ID
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dataply",
3
- "version": "0.0.24-alpha.5",
3
+ "version": "0.0.24-alpha.6",
4
4
  "description": "A lightweight storage engine for Node.js with support for MVCC, WAL.",
5
5
  "license": "MIT",
6
6
  "author": "izure <admin@izure.org>",