dataply 0.0.26-alpha.6 → 0.0.26-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/index.js CHANGED
@@ -5026,32 +5026,73 @@ var BPTreePureSync = class {
5026
5026
  createNode(leaf, keys, values, parent = null, next = null, prev = null) {
5027
5027
  const id = strategy.id(leaf);
5028
5028
  const node = { id, keys, values, leaf, parent, next, prev };
5029
- strategy.write(id, node);
5029
+ return node;
5030
+ },
5031
+ updateNode() {
5032
+ },
5033
+ deleteNode() {
5034
+ },
5035
+ readHead() {
5036
+ return strategy.readHead();
5037
+ },
5038
+ writeHead() {
5039
+ }
5040
+ };
5041
+ }
5042
+ _createBufferedOps() {
5043
+ const strategy = this.strategy;
5044
+ const writeBuffer = /* @__PURE__ */ new Map();
5045
+ const deleteBuffer = /* @__PURE__ */ new Set();
5046
+ let headBuffer = null;
5047
+ const ops = {
5048
+ getNode(id) {
5049
+ const buffered = writeBuffer.get(id);
5050
+ if (buffered) return buffered;
5051
+ return strategy.read(id);
5052
+ },
5053
+ createNode(leaf, keys, values, parent = null, next = null, prev = null) {
5054
+ const id = strategy.id(leaf);
5055
+ const node = { id, keys, values, leaf, parent, next, prev };
5056
+ writeBuffer.set(id, node);
5030
5057
  return node;
5031
5058
  },
5032
5059
  updateNode(node) {
5033
- strategy.write(node.id, node);
5060
+ writeBuffer.set(node.id, node);
5034
5061
  },
5035
5062
  deleteNode(node) {
5036
- strategy.delete(node.id);
5063
+ deleteBuffer.add(node.id);
5064
+ writeBuffer.delete(node.id);
5037
5065
  },
5038
5066
  readHead() {
5067
+ if (headBuffer) return headBuffer;
5039
5068
  return strategy.readHead();
5040
5069
  },
5041
5070
  writeHead(head) {
5042
- strategy.writeHead(head);
5071
+ headBuffer = head;
5043
5072
  }
5044
5073
  };
5074
+ function flush() {
5075
+ for (const id of deleteBuffer) {
5076
+ strategy.delete(id);
5077
+ }
5078
+ for (const [id, node] of writeBuffer) {
5079
+ strategy.write(id, node);
5080
+ }
5081
+ if (headBuffer) {
5082
+ strategy.writeHead(headBuffer);
5083
+ }
5084
+ }
5085
+ return { ops, flush };
5045
5086
  }
5046
5087
  init() {
5047
- this._ops = this._createOps();
5088
+ const { ops, flush } = this._createBufferedOps();
5048
5089
  this._ctx = {
5049
5090
  rootId: "",
5050
5091
  order: this.strategy.order,
5051
5092
  headData: () => this.strategy.head.data
5052
5093
  };
5053
5094
  initOp(
5054
- this._ops,
5095
+ ops,
5055
5096
  this._ctx,
5056
5097
  this.strategy.order,
5057
5098
  this.strategy.head,
@@ -5059,6 +5100,8 @@ var BPTreePureSync = class {
5059
5100
  this.strategy.head = head;
5060
5101
  }
5061
5102
  );
5103
+ flush();
5104
+ this._ops = this._createOps();
5062
5105
  }
5063
5106
  /**
5064
5107
  * Returns the ID of the root node.
@@ -5132,16 +5175,24 @@ var BPTreePureSync = class {
5132
5175
  }
5133
5176
  // ─── Mutation ────────────────────────────────────────────────────
5134
5177
  insert(key, value) {
5135
- insertOp(this._ops, this._ctx, key, value, this.comparator);
5178
+ const { ops, flush } = this._createBufferedOps();
5179
+ insertOp(ops, this._ctx, key, value, this.comparator);
5180
+ flush();
5136
5181
  }
5137
5182
  delete(key, value) {
5138
- deleteOp(this._ops, this._ctx, key, this.comparator, value);
5183
+ const { ops, flush } = this._createBufferedOps();
5184
+ deleteOp(ops, this._ctx, key, this.comparator, value);
5185
+ flush();
5139
5186
  }
5140
5187
  batchInsert(entries) {
5141
- batchInsertOp(this._ops, this._ctx, entries, this.comparator);
5188
+ const { ops, flush } = this._createBufferedOps();
5189
+ batchInsertOp(ops, this._ctx, entries, this.comparator);
5190
+ flush();
5142
5191
  }
5143
5192
  bulkLoad(entries) {
5144
- bulkLoadOp(this._ops, this._ctx, entries, this.comparator);
5193
+ const { ops, flush } = this._createBufferedOps();
5194
+ bulkLoadOp(ops, this._ctx, entries, this.comparator);
5195
+ flush();
5145
5196
  }
5146
5197
  // ─── Head Data ───────────────────────────────────────────────────
5147
5198
  getHeadData() {
@@ -5152,15 +5203,17 @@ var BPTreePureSync = class {
5152
5203
  return head.data;
5153
5204
  }
5154
5205
  setHeadData(data) {
5155
- const head = this._ops.readHead();
5206
+ const { ops, flush } = this._createBufferedOps();
5207
+ const head = ops.readHead();
5156
5208
  if (head === null) {
5157
5209
  throw new Error("Head not found");
5158
5210
  }
5159
- this._ops.writeHead({
5211
+ ops.writeHead({
5160
5212
  root: head.root,
5161
5213
  order: head.order,
5162
5214
  data
5163
5215
  });
5216
+ flush();
5164
5217
  }
5165
5218
  // ─── Static utilities ────────────────────────────────────────────
5166
5219
  static ChooseDriver = BPTreeTransaction.ChooseDriver;
@@ -5169,6 +5222,7 @@ var BPTreePureAsync = class {
5169
5222
  strategy;
5170
5223
  comparator;
5171
5224
  option;
5225
+ lock = new Ryoiki2();
5172
5226
  _cachedRegexp = /* @__PURE__ */ new Map();
5173
5227
  _ctx;
5174
5228
  _ops;
@@ -5194,39 +5248,93 @@ var BPTreePureAsync = class {
5194
5248
  async createNode(leaf, keys, values, parent = null, next = null, prev = null) {
5195
5249
  const id = await strategy.id(leaf);
5196
5250
  const node = { id, keys, values, leaf, parent, next, prev };
5197
- await strategy.write(id, node);
5251
+ return node;
5252
+ },
5253
+ async updateNode() {
5254
+ },
5255
+ async deleteNode() {
5256
+ },
5257
+ async readHead() {
5258
+ return await strategy.readHead();
5259
+ },
5260
+ async writeHead() {
5261
+ }
5262
+ };
5263
+ }
5264
+ _createBufferedOps() {
5265
+ const strategy = this.strategy;
5266
+ const writeBuffer = /* @__PURE__ */ new Map();
5267
+ const deleteBuffer = /* @__PURE__ */ new Set();
5268
+ let headBuffer = null;
5269
+ const ops = {
5270
+ async getNode(id) {
5271
+ const buffered = writeBuffer.get(id);
5272
+ if (buffered) return buffered;
5273
+ return await strategy.read(id);
5274
+ },
5275
+ async createNode(leaf, keys, values, parent = null, next = null, prev = null) {
5276
+ const id = await strategy.id(leaf);
5277
+ const node = { id, keys, values, leaf, parent, next, prev };
5278
+ writeBuffer.set(id, node);
5198
5279
  return node;
5199
5280
  },
5200
5281
  async updateNode(node) {
5201
- await strategy.write(node.id, node);
5282
+ writeBuffer.set(node.id, node);
5202
5283
  },
5203
5284
  async deleteNode(node) {
5204
- await strategy.delete(node.id);
5285
+ deleteBuffer.add(node.id);
5286
+ writeBuffer.delete(node.id);
5205
5287
  },
5206
5288
  async readHead() {
5289
+ if (headBuffer) return headBuffer;
5207
5290
  return await strategy.readHead();
5208
5291
  },
5209
5292
  async writeHead(head) {
5210
- await strategy.writeHead(head);
5293
+ headBuffer = head;
5211
5294
  }
5212
5295
  };
5296
+ async function flush() {
5297
+ for (const id of deleteBuffer) {
5298
+ await strategy.delete(id);
5299
+ }
5300
+ for (const [id, node] of writeBuffer) {
5301
+ await strategy.write(id, node);
5302
+ }
5303
+ if (headBuffer) {
5304
+ await strategy.writeHead(headBuffer);
5305
+ }
5306
+ }
5307
+ return { ops, flush };
5308
+ }
5309
+ async writeLock(fn) {
5310
+ let lockId;
5311
+ return this.lock.writeLock(async (_lockId) => {
5312
+ lockId = _lockId;
5313
+ return fn();
5314
+ }).finally(() => {
5315
+ this.lock.writeUnlock(lockId);
5316
+ });
5213
5317
  }
5214
5318
  async init() {
5215
- this._ops = this._createOps();
5216
- this._ctx = {
5217
- rootId: "",
5218
- order: this.strategy.order,
5219
- headData: () => this.strategy.head.data
5220
- };
5221
- await initOpAsync(
5222
- this._ops,
5223
- this._ctx,
5224
- this.strategy.order,
5225
- this.strategy.head,
5226
- (head) => {
5227
- this.strategy.head = head;
5228
- }
5229
- );
5319
+ return this.writeLock(async () => {
5320
+ const { ops, flush } = this._createBufferedOps();
5321
+ this._ctx = {
5322
+ rootId: "",
5323
+ order: this.strategy.order,
5324
+ headData: () => this.strategy.head.data
5325
+ };
5326
+ await initOpAsync(
5327
+ ops,
5328
+ this._ctx,
5329
+ this.strategy.order,
5330
+ this.strategy.head,
5331
+ (head) => {
5332
+ this.strategy.head = head;
5333
+ }
5334
+ );
5335
+ await flush();
5336
+ this._ops = this._createOps();
5337
+ });
5230
5338
  }
5231
5339
  getRootId() {
5232
5340
  return this._ctx.rootId;
@@ -5250,28 +5358,40 @@ var BPTreePureAsync = class {
5250
5358
  return existsOpAsync(this._ops, this._ctx.rootId, key, value, this.comparator);
5251
5359
  }
5252
5360
  async *keysStream(condition, options) {
5253
- yield* keysStreamOpAsync(
5254
- this._ops,
5255
- this._ctx.rootId,
5256
- condition,
5257
- this.comparator,
5258
- this._verifierMap,
5259
- this._searchConfigs,
5260
- this._ensureValues,
5261
- options
5262
- );
5361
+ let lockId;
5362
+ try {
5363
+ lockId = await this.lock.readLock([0, 0.1], async (id) => id);
5364
+ yield* keysStreamOpAsync(
5365
+ this._ops,
5366
+ this._ctx.rootId,
5367
+ condition,
5368
+ this.comparator,
5369
+ this._verifierMap,
5370
+ this._searchConfigs,
5371
+ this._ensureValues,
5372
+ options
5373
+ );
5374
+ } finally {
5375
+ if (lockId) this.lock.readUnlock(lockId);
5376
+ }
5263
5377
  }
5264
5378
  async *whereStream(condition, options) {
5265
- yield* whereStreamOpAsync(
5266
- this._ops,
5267
- this._ctx.rootId,
5268
- condition,
5269
- this.comparator,
5270
- this._verifierMap,
5271
- this._searchConfigs,
5272
- this._ensureValues,
5273
- options
5274
- );
5379
+ let lockId;
5380
+ try {
5381
+ lockId = await this.lock.readLock([0, 0.1], async (id) => id);
5382
+ yield* whereStreamOpAsync(
5383
+ this._ops,
5384
+ this._ctx.rootId,
5385
+ condition,
5386
+ this.comparator,
5387
+ this._verifierMap,
5388
+ this._searchConfigs,
5389
+ this._ensureValues,
5390
+ options
5391
+ );
5392
+ } finally {
5393
+ if (lockId) this.lock.readUnlock(lockId);
5394
+ }
5275
5395
  }
5276
5396
  async keys(condition, options) {
5277
5397
  const set = /* @__PURE__ */ new Set();
@@ -5289,16 +5409,32 @@ var BPTreePureAsync = class {
5289
5409
  }
5290
5410
  // ─── Mutation ────────────────────────────────────────────────────
5291
5411
  async insert(key, value) {
5292
- await insertOpAsync(this._ops, this._ctx, key, value, this.comparator);
5412
+ return this.writeLock(async () => {
5413
+ const { ops, flush } = this._createBufferedOps();
5414
+ await insertOpAsync(ops, this._ctx, key, value, this.comparator);
5415
+ await flush();
5416
+ });
5293
5417
  }
5294
5418
  async delete(key, value) {
5295
- await deleteOpAsync(this._ops, this._ctx, key, this.comparator, value);
5419
+ return this.writeLock(async () => {
5420
+ const { ops, flush } = this._createBufferedOps();
5421
+ await deleteOpAsync(ops, this._ctx, key, this.comparator, value);
5422
+ await flush();
5423
+ });
5296
5424
  }
5297
5425
  async batchInsert(entries) {
5298
- await batchInsertOpAsync(this._ops, this._ctx, entries, this.comparator);
5426
+ return this.writeLock(async () => {
5427
+ const { ops, flush } = this._createBufferedOps();
5428
+ await batchInsertOpAsync(ops, this._ctx, entries, this.comparator);
5429
+ await flush();
5430
+ });
5299
5431
  }
5300
5432
  async bulkLoad(entries) {
5301
- await bulkLoadOpAsync(this._ops, this._ctx, entries, this.comparator);
5433
+ return this.writeLock(async () => {
5434
+ const { ops, flush } = this._createBufferedOps();
5435
+ await bulkLoadOpAsync(ops, this._ctx, entries, this.comparator);
5436
+ await flush();
5437
+ });
5302
5438
  }
5303
5439
  // ─── Head Data ───────────────────────────────────────────────────
5304
5440
  async getHeadData() {
@@ -5307,9 +5443,13 @@ var BPTreePureAsync = class {
5307
5443
  return head.data;
5308
5444
  }
5309
5445
  async setHeadData(data) {
5310
- const head = await this._ops.readHead();
5311
- if (head === null) throw new Error("Head not found");
5312
- await this._ops.writeHead({ root: head.root, order: head.order, data });
5446
+ return this.writeLock(async () => {
5447
+ const { ops, flush } = this._createBufferedOps();
5448
+ const head = await ops.readHead();
5449
+ if (head === null) throw new Error("Head not found");
5450
+ await ops.writeHead({ root: head.root, order: head.order, data });
5451
+ await flush();
5452
+ });
5313
5453
  }
5314
5454
  // ─── Static utilities ────────────────────────────────────────────
5315
5455
  static ChooseDriver = BPTreeTransaction.ChooseDriver;
@@ -9767,11 +9907,7 @@ var PageFileSystem = class {
9767
9907
  * @returns 페이지 버퍼
9768
9908
  */
9769
9909
  async get(pageIndex, tx) {
9770
- const page = await tx.readPage(pageIndex);
9771
- if (page === null) {
9772
- return new Uint8Array(this.pageSize);
9773
- }
9774
- return page;
9910
+ return tx.__readPage(pageIndex);
9775
9911
  }
9776
9912
  /**
9777
9913
  * Reads the page header.
@@ -9862,7 +9998,7 @@ var PageFileSystem = class {
9862
9998
  const manager = this.pageFactory.getManager(page);
9863
9999
  manager.updateChecksum(page);
9864
10000
  await tx.__acquireWriteLock(pageIndex);
9865
- await tx.writePage(pageIndex, page);
10001
+ await tx.__writePage(pageIndex, page);
9866
10002
  }
9867
10003
  /**
9868
10004
  * Appends and inserts a new page.
@@ -10831,6 +10967,7 @@ var Transaction = class {
10831
10967
  this.pfs = pfs;
10832
10968
  this.id = id;
10833
10969
  this.rootTx = rootTx;
10970
+ this.mvccTx = rootTx.createNested();
10834
10971
  }
10835
10972
  /** Transaction ID */
10836
10973
  id;
@@ -10841,22 +10978,11 @@ var Transaction = class {
10841
10978
  /** List of callbacks to execute on commit */
10842
10979
  commitHooks = [];
10843
10980
  /** Nested MVCC Transaction for snapshot isolation (lazy init) */
10844
- mvccTx = null;
10981
+ mvccTx;
10845
10982
  /** Root MVCC Transaction reference */
10846
10983
  rootTx;
10847
10984
  /** Release function for global write lock, set by DataplyAPI */
10848
10985
  _writeLockRelease = null;
10849
- /**
10850
- * Lazily initializes the nested MVCC transaction.
10851
- * This ensures the snapshot is taken at the time of first access,
10852
- * picking up the latest committed root version.
10853
- */
10854
- ensureMvccTx() {
10855
- if (!this.mvccTx) {
10856
- this.mvccTx = this.rootTx.createNested();
10857
- }
10858
- return this.mvccTx;
10859
- }
10860
10986
  /**
10861
10987
  * Registers a commit hook.
10862
10988
  * @param hook Function to execute
@@ -10882,9 +11008,8 @@ var Transaction = class {
10882
11008
  * @param pageId Page ID
10883
11009
  * @returns Page data
10884
11010
  */
10885
- async readPage(pageId) {
10886
- const tx = this.ensureMvccTx();
10887
- const data = await tx.read(pageId);
11011
+ async __readPage(pageId) {
11012
+ const data = await this.mvccTx.read(pageId);
10888
11013
  if (data === null) {
10889
11014
  return new Uint8Array(this.pfs.pageSize);
10890
11015
  }
@@ -10897,15 +11022,14 @@ var Transaction = class {
10897
11022
  * @param pageId Page ID
10898
11023
  * @param data Page data
10899
11024
  */
10900
- async writePage(pageId, data) {
10901
- const tx = this.ensureMvccTx();
10902
- const exists = await tx.exists(pageId);
11025
+ async __writePage(pageId, data) {
11026
+ const exists = await this.mvccTx.exists(pageId);
10903
11027
  if (exists) {
10904
11028
  const copy = new Uint8Array(data.length);
10905
11029
  copy.set(data);
10906
- await tx.write(pageId, copy);
11030
+ await this.mvccTx.write(pageId, copy);
10907
11031
  } else {
10908
- await tx.create(pageId, data);
11032
+ await this.mvccTx.create(pageId, data);
10909
11033
  }
10910
11034
  }
10911
11035
  /**
@@ -10933,10 +11057,9 @@ var Transaction = class {
10933
11057
  await hook();
10934
11058
  }
10935
11059
  });
10936
- const tx = this.ensureMvccTx();
10937
11060
  let shouldTriggerCheckpoint = false;
10938
11061
  await this.pfs.runGlobalLock(async () => {
10939
- const entries = tx.getResultEntries();
11062
+ const entries = this.mvccTx.getResultEntries();
10940
11063
  const dirtyPages = /* @__PURE__ */ new Map();
10941
11064
  for (const entry of [...entries.created, ...entries.updated]) {
10942
11065
  dirtyPages.set(entry.key, entry.data);
@@ -10946,7 +11069,7 @@ var Transaction = class {
10946
11069
  await this.pfs.wal.prepareCommit(dirtyPages);
10947
11070
  await this.pfs.wal.writeCommitMarker();
10948
11071
  }
10949
- await tx.commit();
11072
+ await this.mvccTx.commit();
10950
11073
  if (hasDirtyPages) {
10951
11074
  await this.rootTx.commit();
10952
11075
  }
@@ -11079,7 +11202,6 @@ var DataplyAPI = class {
11079
11202
  constructor(file, options) {
11080
11203
  this.file = file;
11081
11204
  this.hook = useHookall(this);
11082
- this.latcher = new Ryoiki3();
11083
11205
  this.options = this.verboseOptions(options);
11084
11206
  this.loggerManager = new LoggerManager(this.options.logLevel);
11085
11207
  this.logger = this.loggerManager.create("DataplyAPI");
@@ -11132,38 +11254,6 @@ var DataplyAPI = class {
11132
11254
  txIdCounter;
11133
11255
  /** Promise-chain mutex for serializing write operations */
11134
11256
  writeQueue = Promise.resolve();
11135
- /** Lock manager. Used for managing transactions */
11136
- latcher;
11137
- /**
11138
- * Acquire a read lock on the given page ID and execute the given function.
11139
- * @param pageId Page ID to acquire a read lock on
11140
- * @param fn Function to execute while holding the read lock
11141
- * @returns The result of the given function
11142
- */
11143
- async latchReadLock(pageId, fn) {
11144
- let lockId;
11145
- return this.latcher.readLock(this.latcher.range(pageId, 1), async (_lockId) => {
11146
- lockId = _lockId;
11147
- return fn();
11148
- }).finally(() => {
11149
- this.latcher.readUnlock(lockId);
11150
- });
11151
- }
11152
- /**
11153
- * Acquire a write lock on the given page ID and execute the given function.
11154
- * @param pageId Page ID to acquire a write lock on
11155
- * @param fn Function to execute while holding the write lock
11156
- * @returns The result of the given function
11157
- */
11158
- async latchWriteLock(pageId, fn) {
11159
- let lockId;
11160
- return this.latcher.writeLock(this.latcher.range(pageId, 1), async (_lockId) => {
11161
- lockId = _lockId;
11162
- return fn();
11163
- }).finally(() => {
11164
- this.latcher.writeUnlock(lockId);
11165
- });
11166
- }
11167
11257
  /**
11168
11258
  * Verifies if the page file is a valid Dataply file.
11169
11259
  * The metadata page must be located at the beginning of the Dataply file.
@@ -11293,7 +11383,7 @@ var DataplyAPI = class {
11293
11383
  if (this.initialized) {
11294
11384
  return;
11295
11385
  }
11296
- await this.runWithDefault(async (tx) => {
11386
+ await this.withReadTransaction(async (tx) => {
11297
11387
  await this.hook.trigger("init", tx, async (tx2) => {
11298
11388
  await this.pfs.init();
11299
11389
  await this.rowTableEngine.init();
@@ -11318,15 +11408,6 @@ var DataplyAPI = class {
11318
11408
  this.pfs
11319
11409
  );
11320
11410
  }
11321
- /**
11322
- * Runs a callback function within a transaction context.
11323
- * If no transaction is provided, a new transaction is created.
11324
- * The transaction is committed if the callback completes successfully,
11325
- * or rolled back if an error occurs.
11326
- * @param callback The callback function to run within the transaction context.
11327
- * @param tx The transaction to use. If not provided, a new transaction is created.
11328
- * @returns The result of the callback function.
11329
- */
11330
11411
  /**
11331
11412
  * Acquires the global write lock.
11332
11413
  * Returns a release function that MUST be called to unlock.
@@ -11351,7 +11432,7 @@ var DataplyAPI = class {
11351
11432
  * @param tx Optional external transaction.
11352
11433
  * @returns The result of the callback.
11353
11434
  */
11354
- async runWithDefaultWrite(callback, tx) {
11435
+ async withWriteTransaction(callback, tx) {
11355
11436
  this.logger.debug("Running with default write transaction");
11356
11437
  if (!tx) {
11357
11438
  const release = await this.acquireWriteLock();
@@ -11375,7 +11456,7 @@ var DataplyAPI = class {
11375
11456
  }
11376
11457
  return result;
11377
11458
  }
11378
- async runWithDefault(callback, tx) {
11459
+ async withReadTransaction(callback, tx) {
11379
11460
  this.logger.debug("Running with default transaction");
11380
11461
  const isInternalTx = !tx;
11381
11462
  if (!tx) {
@@ -11403,7 +11484,7 @@ var DataplyAPI = class {
11403
11484
  * @param tx The transaction to use. If not provided, a new transaction is created.
11404
11485
  * @returns An AsyncGenerator that yields values from the callback.
11405
11486
  */
11406
- async *streamWithDefault(callback, tx) {
11487
+ async *withReadStreamTransaction(callback, tx) {
11407
11488
  this.logger.debug("Streaming with default transaction");
11408
11489
  const isInternalTx = !tx;
11409
11490
  if (!tx) {
@@ -11436,7 +11517,7 @@ var DataplyAPI = class {
11436
11517
  if (!this.initialized) {
11437
11518
  throw new Error("Dataply instance is not initialized");
11438
11519
  }
11439
- return this.runWithDefault((tx2) => this.rowTableEngine.getMetadata(tx2), tx);
11520
+ return this.withReadTransaction((tx2) => this.rowTableEngine.getMetadata(tx2), tx);
11440
11521
  }
11441
11522
  /**
11442
11523
  * Inserts data. Returns the PK of the added row.
@@ -11450,7 +11531,7 @@ var DataplyAPI = class {
11450
11531
  if (!this.initialized) {
11451
11532
  throw new Error("Dataply instance is not initialized");
11452
11533
  }
11453
- return this.runWithDefaultWrite(async (tx2) => {
11534
+ return this.withWriteTransaction(async (tx2) => {
11454
11535
  incrementRowCount = incrementRowCount ?? true;
11455
11536
  if (typeof data === "string") {
11456
11537
  data = this.textCodec.encode(data);
@@ -11471,7 +11552,7 @@ var DataplyAPI = class {
11471
11552
  if (!this.initialized) {
11472
11553
  throw new Error("Dataply instance is not initialized");
11473
11554
  }
11474
- return this.runWithDefaultWrite(async (tx2) => {
11555
+ return this.withWriteTransaction(async (tx2) => {
11475
11556
  incrementRowCount = incrementRowCount ?? true;
11476
11557
  if (typeof data === "string") {
11477
11558
  data = this.textCodec.encode(data);
@@ -11493,7 +11574,7 @@ var DataplyAPI = class {
11493
11574
  if (!this.initialized) {
11494
11575
  throw new Error("Dataply instance is not initialized");
11495
11576
  }
11496
- return this.runWithDefaultWrite(async (tx2) => {
11577
+ return this.withWriteTransaction(async (tx2) => {
11497
11578
  incrementRowCount = incrementRowCount ?? true;
11498
11579
  const encodedList = dataList.map(
11499
11580
  (data) => typeof data === "string" ? this.textCodec.encode(data) : data
@@ -11512,7 +11593,7 @@ var DataplyAPI = class {
11512
11593
  if (!this.initialized) {
11513
11594
  throw new Error("Dataply instance is not initialized");
11514
11595
  }
11515
- return this.runWithDefaultWrite(async (tx2) => {
11596
+ return this.withWriteTransaction(async (tx2) => {
11516
11597
  if (typeof data === "string") {
11517
11598
  data = this.textCodec.encode(data);
11518
11599
  }
@@ -11530,7 +11611,7 @@ var DataplyAPI = class {
11530
11611
  if (!this.initialized) {
11531
11612
  throw new Error("Dataply instance is not initialized");
11532
11613
  }
11533
- return this.runWithDefaultWrite(async (tx2) => {
11614
+ return this.withWriteTransaction(async (tx2) => {
11534
11615
  decrementRowCount = decrementRowCount ?? true;
11535
11616
  await this.rowTableEngine.delete(pk, decrementRowCount, tx2);
11536
11617
  }, tx);
@@ -11540,7 +11621,7 @@ var DataplyAPI = class {
11540
11621
  if (!this.initialized) {
11541
11622
  throw new Error("Dataply instance is not initialized");
11542
11623
  }
11543
- return this.runWithDefault(async (tx2) => {
11624
+ return this.withReadTransaction(async (tx2) => {
11544
11625
  const data = await this.rowTableEngine.selectByPK(pk, tx2);
11545
11626
  if (data === null) return null;
11546
11627
  if (asRaw) return data;
@@ -11552,7 +11633,7 @@ var DataplyAPI = class {
11552
11633
  if (!this.initialized) {
11553
11634
  throw new Error("Dataply instance is not initialized");
11554
11635
  }
11555
- return this.runWithDefault(async (tx2) => {
11636
+ return this.withReadTransaction(async (tx2) => {
11556
11637
  const results = await this.rowTableEngine.selectMany(pks, tx2);
11557
11638
  return results.map((data) => {
11558
11639
  if (data === null) return null;
@@ -11569,7 +11650,7 @@ var DataplyAPI = class {
11569
11650
  if (!this.initialized) {
11570
11651
  throw new Error("Dataply instance is not initialized");
11571
11652
  }
11572
- return this.runWithDefaultWrite(() => {
11653
+ return this.withWriteTransaction(() => {
11573
11654
  return this.hook.trigger("close", void 0, async () => {
11574
11655
  await this.pfs.close();
11575
11656
  import_node_fs3.default.closeSync(this.fileHandle);
@@ -11600,6 +11681,24 @@ var Dataply = class {
11600
11681
  createTransaction() {
11601
11682
  return this.api.createTransaction();
11602
11683
  }
11684
+ /**
11685
+ * Runs a write callback within a transaction context.
11686
+ */
11687
+ async withWriteTransaction(callback, tx) {
11688
+ return this.api.withWriteTransaction(callback, tx);
11689
+ }
11690
+ /**
11691
+ * Runs a read callback within a transaction context.
11692
+ */
11693
+ async withReadTransaction(callback, tx) {
11694
+ return this.api.withReadTransaction(callback, tx);
11695
+ }
11696
+ /**
11697
+ * Runs a generator callback function within a transaction context.
11698
+ */
11699
+ async *withReadStreamTransaction(callback, tx) {
11700
+ return this.api.withReadStreamTransaction(callback, tx);
11701
+ }
11603
11702
  /**
11604
11703
  * Initializes the dataply instance.
11605
11704
  * Must be called before using the dataply instance.
@@ -11666,10 +11765,42 @@ var Dataply = class {
11666
11765
  };
11667
11766
 
11668
11767
  // src/core/transaction/GlobalTransaction.ts
11669
- var GlobalTransaction = class {
11768
+ var GlobalTransaction = class _GlobalTransaction {
11670
11769
  transactions = [];
11671
11770
  isCommitted = false;
11672
11771
  isRolledBack = false;
11772
+ /**
11773
+ * Executes a global transaction across multiple Dataply instances using a callback.
11774
+ * Locks are acquired in the order instances are provided.
11775
+ * @param dbs Array of Dataply instances
11776
+ * @param callback Function to execute with the array of Transactions
11777
+ */
11778
+ static async Run(dbs, callback) {
11779
+ const globalTx = new _GlobalTransaction();
11780
+ const txs = [];
11781
+ const releases = [];
11782
+ try {
11783
+ for (const db of dbs) {
11784
+ const release = await db.api.acquireWriteLock();
11785
+ releases.push(release);
11786
+ const tx = db.api.createTransaction();
11787
+ tx.__setWriteLockRelease(release);
11788
+ txs.push(tx);
11789
+ globalTx.add(tx);
11790
+ }
11791
+ const result = await callback(txs);
11792
+ await globalTx.commit();
11793
+ return result;
11794
+ } catch (e) {
11795
+ await globalTx.rollback();
11796
+ for (let i = txs.length; i < releases.length; i++) {
11797
+ releases[i]();
11798
+ }
11799
+ throw e;
11800
+ }
11801
+ }
11802
+ constructor() {
11803
+ }
11673
11804
  /**
11674
11805
  * Adds a transaction to the global transaction.
11675
11806
  * @param tx Transaction to add
@@ -18,7 +18,19 @@ export declare class Dataply {
18
18
  * A transaction must be terminated by calling either `commit` or `rollback`.
19
19
  * @returns Transaction object
20
20
  */
21
- createTransaction(): Transaction;
21
+ protected createTransaction(): Transaction;
22
+ /**
23
+ * Runs a write callback within a transaction context.
24
+ */
25
+ withWriteTransaction<T>(callback: (tx: Transaction) => Promise<T>, tx?: Transaction): Promise<T>;
26
+ /**
27
+ * Runs a read callback within a transaction context.
28
+ */
29
+ withReadTransaction<T>(callback: (tx: Transaction) => Promise<T>, tx?: Transaction): Promise<T>;
30
+ /**
31
+ * Runs a generator callback function within a transaction context.
32
+ */
33
+ withReadStreamTransaction<T>(callback: (tx: Transaction) => AsyncGenerator<T>, tx?: Transaction): AsyncGenerator<T>;
22
34
  /**
23
35
  * Initializes the dataply instance.
24
36
  * Must be called before using the dataply instance.
@@ -1,6 +1,5 @@
1
1
  import { type DataplyOptions, type DataplyMetadata } from '../types';
2
2
  import { type IHookall } from 'hookall';
3
- import { Ryoiki } from 'ryoiki';
4
3
  import { PageFileSystem } from './PageFileSystem';
5
4
  import { RowTableEngine } from './RowTableEngine';
6
5
  import { TextCodec } from '../utils/TextCodec';
@@ -48,23 +47,7 @@ export declare class DataplyAPI {
48
47
  private txIdCounter;
49
48
  /** Promise-chain mutex for serializing write operations */
50
49
  private writeQueue;
51
- /** Lock manager. Used for managing transactions */
52
- protected readonly latcher: Ryoiki;
53
50
  constructor(file: string, options: DataplyOptions);
54
- /**
55
- * Acquire a read lock on the given page ID and execute the given function.
56
- * @param pageId Page ID to acquire a read lock on
57
- * @param fn Function to execute while holding the read lock
58
- * @returns The result of the given function
59
- */
60
- latchReadLock<T>(pageId: number, fn: () => Promise<T>): Promise<T>;
61
- /**
62
- * Acquire a write lock on the given page ID and execute the given function.
63
- * @param pageId Page ID to acquire a write lock on
64
- * @param fn Function to execute while holding the write lock
65
- * @returns The result of the given function
66
- */
67
- latchWriteLock<T>(pageId: number, fn: () => Promise<T>): Promise<T>;
68
51
  /**
69
52
  * Verifies if the page file is a valid Dataply file.
70
53
  * The metadata page must be located at the beginning of the Dataply file.
@@ -106,22 +89,13 @@ export declare class DataplyAPI {
106
89
  * @returns Transaction object
107
90
  */
108
91
  createTransaction(): Transaction;
109
- /**
110
- * Runs a callback function within a transaction context.
111
- * If no transaction is provided, a new transaction is created.
112
- * The transaction is committed if the callback completes successfully,
113
- * or rolled back if an error occurs.
114
- * @param callback The callback function to run within the transaction context.
115
- * @param tx The transaction to use. If not provided, a new transaction is created.
116
- * @returns The result of the callback function.
117
- */
118
92
  /**
119
93
  * Acquires the global write lock.
120
94
  * Returns a release function that MUST be called to unlock.
121
95
  * Used internally by runWithDefaultWrite.
122
96
  * @returns A release function
123
97
  */
124
- protected acquireWriteLock(): Promise<() => void>;
98
+ acquireWriteLock(): Promise<() => void>;
125
99
  /**
126
100
  * Runs a write callback within a transaction context with global write serialization.
127
101
  * If no transaction is provided, a new transaction is created, committed on success, rolled back on error.
@@ -131,8 +105,8 @@ export declare class DataplyAPI {
131
105
  * @param tx Optional external transaction.
132
106
  * @returns The result of the callback.
133
107
  */
134
- protected runWithDefaultWrite<T>(callback: (tx: Transaction) => Promise<T>, tx?: Transaction): Promise<T>;
135
- protected runWithDefault<T>(callback: (tx: Transaction) => Promise<T>, tx?: Transaction): Promise<T>;
108
+ withWriteTransaction<T>(callback: (tx: Transaction) => Promise<T>, tx?: Transaction): Promise<T>;
109
+ withReadTransaction<T>(callback: (tx: Transaction) => Promise<T>, tx?: Transaction): Promise<T>;
136
110
  /**
137
111
  * Runs a generator callback function within a transaction context.
138
112
  * Similar to runWithDefault but allows yielding values from an AsyncGenerator.
@@ -143,7 +117,7 @@ export declare class DataplyAPI {
143
117
  * @param tx The transaction to use. If not provided, a new transaction is created.
144
118
  * @returns An AsyncGenerator that yields values from the callback.
145
119
  */
146
- protected streamWithDefault<T>(callback: (tx: Transaction) => AsyncGenerator<T>, tx?: Transaction): AsyncGenerator<T>;
120
+ withReadStreamTransaction<T>(callback: (tx: Transaction) => AsyncGenerator<T>, tx?: Transaction): AsyncGenerator<T>;
147
121
  /**
148
122
  * Retrieves metadata from the dataply.
149
123
  * @returns Metadata of the dataply.
@@ -1,4 +1,5 @@
1
1
  import { Transaction } from './Transaction';
2
+ import { Dataply } from '../Dataply';
2
3
  /**
3
4
  * Global Transaction Manager.
4
5
  * Coordinates transactions across multiple instances (shards).
@@ -9,19 +10,27 @@ export declare class GlobalTransaction {
9
10
  private transactions;
10
11
  private isCommitted;
11
12
  private isRolledBack;
13
+ /**
14
+ * Executes a global transaction across multiple Dataply instances using a callback.
15
+ * Locks are acquired in the order instances are provided.
16
+ * @param dbs Array of Dataply instances
17
+ * @param callback Function to execute with the array of Transactions
18
+ */
19
+ static Run<T>(dbs: Dataply[], callback: (txs: Transaction[]) => Promise<T>): Promise<T>;
20
+ protected constructor();
12
21
  /**
13
22
  * Adds a transaction to the global transaction.
14
23
  * @param tx Transaction to add
15
24
  */
16
- add(tx: Transaction): void;
25
+ protected add(tx: Transaction): void;
17
26
  /**
18
27
  * Commits all transactions.
19
28
  * Note: This is now a single-phase commit. For true atomicity across shards,
20
29
  * each instance's WAL provides durability, but cross-shard atomicity is best-effort.
21
30
  */
22
- commit(): Promise<void>;
31
+ protected commit(): Promise<void>;
23
32
  /**
24
33
  * Rolls back all transactions.
25
34
  */
26
- rollback(): Promise<void>;
35
+ protected rollback(): Promise<void>;
27
36
  }
@@ -21,7 +21,7 @@ export declare class Transaction {
21
21
  /** List of callbacks to execute on commit */
22
22
  private commitHooks;
23
23
  /** Nested MVCC Transaction for snapshot isolation (lazy init) */
24
- private mvccTx;
24
+ private readonly mvccTx;
25
25
  /** Root MVCC Transaction reference */
26
26
  private readonly rootTx;
27
27
  /** Release function for global write lock, set by DataplyAPI */
@@ -34,12 +34,6 @@ export declare class Transaction {
34
34
  * @param pfs Page File System
35
35
  */
36
36
  constructor(id: number, context: TransactionContext, rootTx: AsyncMVCCTransaction<PageMVCCStrategy, number, Uint8Array>, lockManager: LockManager, pfs: PageFileSystem);
37
- /**
38
- * Lazily initializes the nested MVCC transaction.
39
- * This ensures the snapshot is taken at the time of first access,
40
- * picking up the latest committed root version.
41
- */
42
- private ensureMvccTx;
43
37
  /**
44
38
  * Registers a commit hook.
45
39
  * @param hook Function to execute
@@ -59,13 +53,13 @@ export declare class Transaction {
59
53
  * @param pageId Page ID
60
54
  * @returns Page data
61
55
  */
62
- readPage(pageId: number): Promise<Uint8Array>;
56
+ __readPage(pageId: number): Promise<Uint8Array>;
63
57
  /**
64
58
  * Writes a page through the MVCC transaction.
65
59
  * @param pageId Page ID
66
60
  * @param data Page data
67
61
  */
68
- writePage(pageId: number, data: Uint8Array): Promise<void>;
62
+ __writePage(pageId: number, data: Uint8Array): Promise<void>;
69
63
  /**
70
64
  * Acquires a write lock.
71
65
  * @param pageId Page ID
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dataply",
3
- "version": "0.0.26-alpha.6",
3
+ "version": "0.0.26-alpha.8",
4
4
  "description": "A lightweight storage engine for Node.js with support for MVCC, WAL.",
5
5
  "license": "MIT",
6
6
  "author": "izure <admin@izure.org>",
@@ -49,6 +49,6 @@
49
49
  "hookall": "^2.2.0",
50
50
  "mvcc-api": "^1.3.7",
51
51
  "ryoiki": "^1.2.0",
52
- "serializable-bptree": "^9.0.0"
52
+ "serializable-bptree": "^9.0.1"
53
53
  }
54
54
  }
package/readme.md CHANGED
@@ -105,23 +105,17 @@ db.init().then(() => {
105
105
  ## Transaction Management
106
106
 
107
107
  ### Explicit Transactions
108
- You can group multiple operations into a single unit of work to ensure atomicity.
108
+ You can group multiple operations into a single unit of work to ensure atomicity. The `withWriteTransaction` method handles the transaction lifecycle automatically, committing on success and rolling back on failure.
109
109
 
110
110
  ```typescript
111
- const tx = dataply.createTransaction()
112
-
113
- try {
114
- await dataply.insert('Data 1', tx)
111
+ await dataply.withWriteTransaction(async (tx) => {
112
+ const pk = await dataply.insert('Data 1', tx)
115
113
  await dataply.update(pk, 'Updated Data', tx)
116
-
117
- await tx.commit() // Persist changes to disk and clear WAL on success
118
- } catch (error) {
119
- await tx.rollback() // Revert all changes on failure (Undo)
120
- }
114
+ }) // Persists changes automatically on success or rolls back on failure
121
115
  ```
122
116
 
123
117
  ### Global Transactions
124
- You can perform atomic operations across multiple `Dataply` instances using the `GlobalTransaction` class. This uses a **2-Phase Commit (2PC)** mechanism to ensure that either all instances commit successfully or all are rolled back.
118
+ You can perform atomic operations across multiple `Dataply` instances using the `GlobalTransaction` class. This safely acquires write locks on all instances sequentially and manages the transaction lifecycle to ensure either all instances commit successfully or all are rolled back.
125
119
 
126
120
  ```typescript
127
121
  import { Dataply, GlobalTransaction } from 'dataply'
@@ -132,22 +126,13 @@ const db2 = new Dataply('./db2.db', { wal: './db2.wal' })
132
126
  await db1.init()
133
127
  await db2.init()
134
128
 
135
- const tx1 = db1.createTransaction()
136
- const tx2 = db2.createTransaction()
137
-
138
- const globalTx = new GlobalTransaction()
139
- globalTx.add(tx1)
140
- globalTx.add(tx2)
141
-
142
129
  try {
143
- await db1.insert('Data for DB1', tx1)
144
- await db2.insert('Data for DB2', tx2)
145
-
146
- // Commit transactions across all instances
147
- // Note: This is a best-effort atomic commit.
148
- await globalTx.commit()
130
+ await GlobalTransaction.Run([db1, db2], async ([tx1, tx2]) => {
131
+ await db1.insert('Data for DB1', tx1)
132
+ await db2.insert('Data for DB2', tx2)
133
+ })
149
134
  } catch (error) {
150
- await globalTx.rollback()
135
+ console.error('Global transaction failed and rolled back.', error)
151
136
  }
152
137
  ```
153
138
 
@@ -196,30 +181,22 @@ Marks data as deleted.
196
181
  #### `async getMetadata(tx?: Transaction): Promise<DataplyMetadata>`
197
182
  Returns the current metadata of the dataply, including `pageSize`, `pageCount`, and `rowCount`.
198
183
 
199
- #### `createTransaction(): Transaction`
200
- Creates a new transaction instance.
184
+ #### `async withWriteTransaction<T>(callback: (tx: Transaction) => Promise<T>, tx?: Transaction): Promise<T>`
185
+ Executes write operations within a serialized write-lock transaction. Automatically commits on success and rolls back on failure if creating a new internal transaction.
201
186
 
202
- #### `async close(): Promise<void>`
203
- Closes the file handles and shuts down safely.
204
-
205
- ### Transaction Class
187
+ #### `async withReadTransaction<T>(callback: (tx: Transaction) => Promise<T>, tx?: Transaction): Promise<T>`
188
+ Executes read operations within a transaction context.
206
189
 
207
- #### `async commit(): Promise<void>`
208
- Permanently reflects all changes made during the transaction to disk and releases locks.
190
+ #### `async *withReadStreamTransaction<T>(callback: (tx: Transaction) => AsyncGenerator<T>, tx?: Transaction): AsyncGenerator<T>`
191
+ Executes streaming read operations using an async generator in a transaction.
209
192
 
210
- #### `async rollback(): Promise<void>`
211
- Cancels all changes made during the transaction and restores the original state.
193
+ #### `async close(): Promise<void>`
194
+ Closes the file handles and shuts down safely.
212
195
 
213
196
  ### GlobalTransaction Class
214
197
 
215
- #### `add(tx: Transaction): void`
216
- Registers a transaction from a Dataply instance to the global unit.
217
-
218
- #### `async commit(): Promise<void>`
219
- Executes a coordinated commit across all registered transactions. Note that without a prepare phase, this is a best-effort atomic commit.
220
-
221
- #### `async rollback(): Promise<void>`
222
- Rolls back all registered transactions simultaneously.
198
+ #### `static async Run<T>(dbs: Dataply[], callback: (txs: Transaction[]) => Promise<T>): Promise<T>`
199
+ Executes a callback-based global transaction across multiple Dataply instances sequentially locking them to prevent deadlocks, providing true atomicity within the Dataply nodes. Automatically commits on success and rolls back on failure.
223
200
 
224
201
  ## Extending Dataply
225
202