dataply 0.0.26-alpha.5 → 0.0.26-alpha.7

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;
@@ -9553,45 +9693,27 @@ var WALManager = class {
9553
9693
 
9554
9694
  // src/core/PageMVCCStrategy.ts
9555
9695
  var import_node_fs2 = __toESM(require("node:fs"));
9556
- var PageMVCCStrategy = class {
9557
- constructor(fileHandle, pageSize, cacheCapacity) {
9696
+ var PageMVCCStrategy = class extends AsyncMVCCStrategy2 {
9697
+ constructor(fileHandle, pageSize) {
9698
+ super();
9558
9699
  this.fileHandle = fileHandle;
9559
9700
  this.pageSize = pageSize;
9560
- this.cache = new LRUMap2(cacheCapacity);
9561
9701
  this.fileSize = import_node_fs2.default.fstatSync(fileHandle).size;
9562
9702
  }
9563
- /** LRU 캐시 (페이지 ID -> 페이지 데이터) */
9564
- cache;
9565
- /** 디스크에 기록되지 않은 변경된 페이지들 (페이지 ID -> 페이지 데이터) */
9566
- dirtyPages = /* @__PURE__ */ new Map();
9567
9703
  /** 파일 크기 (논리적) */
9568
9704
  fileSize;
9569
9705
  /**
9570
9706
  * 디스크에서 페이지를 읽습니다.
9571
- * 캐시에 있으면 캐시에서 반환합니다.
9572
9707
  * @param pageId 페이지 ID
9573
9708
  * @returns 페이지 데이터
9574
9709
  */
9575
9710
  async read(pageId) {
9576
- const dirty = this.dirtyPages.get(pageId);
9577
- if (dirty) {
9578
- const copy = new Uint8Array(this.pageSize);
9579
- copy.set(dirty);
9580
- return copy;
9581
- }
9582
- const cached = this.cache.get(pageId);
9583
- if (cached) {
9584
- return cached;
9585
- }
9586
9711
  const buffer = new Uint8Array(this.pageSize);
9587
9712
  const pageStartPos = pageId * this.pageSize;
9588
9713
  if (pageStartPos >= this.fileSize) {
9589
9714
  return buffer;
9590
9715
  }
9591
9716
  await this._readFromDisk(buffer, pageStartPos);
9592
- const cacheCopy = new Uint8Array(this.pageSize);
9593
- cacheCopy.set(buffer);
9594
- this.cache.set(pageId, cacheCopy);
9595
9717
  return buffer;
9596
9718
  }
9597
9719
  /**
@@ -9601,47 +9723,27 @@ var PageMVCCStrategy = class {
9601
9723
  */
9602
9724
  async write(pageId, data) {
9603
9725
  const pageStartPos = pageId * this.pageSize;
9604
- const dataCopy = new Uint8Array(this.pageSize);
9605
- dataCopy.set(data);
9606
- this.dirtyPages.set(pageId, dataCopy);
9607
- this.cache.set(pageId, dataCopy);
9726
+ await this._writeToDisk(data, pageStartPos);
9608
9727
  const endPosition = pageStartPos + this.pageSize;
9609
9728
  if (endPosition > this.fileSize) {
9610
9729
  this.fileSize = endPosition;
9611
9730
  }
9612
9731
  }
9613
9732
  /**
9614
- * 더티 페이지들을 메인 디스크 파일에 일괄 기록합니다.
9615
- * WAL 체크포인트 시점에 호출되어야 합니다.
9733
+ * 페이지 삭제.
9734
+ * 실제 페이지 해제는 상위 레이어(FreeList)에서 관리합니다.
9735
+ * @param pageId 페이지 ID
9616
9736
  */
9617
- async flush() {
9618
- if (this.dirtyPages.size === 0) {
9619
- return;
9620
- }
9621
- const snapshot = new Map(this.dirtyPages);
9622
- const sortedPageIds = Array.from(snapshot.keys()).sort((a, b) => a - b);
9623
- for (const pageId of sortedPageIds) {
9624
- const data = snapshot.get(pageId);
9625
- const position = pageId * this.pageSize;
9626
- await this._writeToDisk(data, position);
9627
- this.dirtyPages.delete(pageId);
9628
- }
9737
+ async delete(pageId) {
9629
9738
  }
9630
9739
  /**
9631
- * 지정된 페이지들만 디스크에 기록합니다.
9632
- * WAL 없이 트랜잭션 커밋 시, 해당 트랜잭션의 dirty pages만 선택적으로 flush합니다.
9633
- * @param pages 기록할 페이지 맵 (PageID -> PageData)
9740
+ * 페이지 존재 여부 확인
9741
+ * @param pageId 페이지 ID
9742
+ * @returns 존재하면 true
9634
9743
  */
9635
- async flushPages(pages) {
9636
- if (pages.size === 0) {
9637
- return;
9638
- }
9639
- const sortedPageIds = Array.from(pages.keys()).sort((a, b) => a - b);
9640
- for (const pageId of sortedPageIds) {
9641
- const data = pages.get(pageId);
9642
- const position = pageId * this.pageSize;
9643
- await this._writeToDisk(data, position);
9644
- }
9744
+ async exists(pageId) {
9745
+ const pageStartPos = pageId * this.pageSize;
9746
+ return pageStartPos < this.fileSize;
9645
9747
  }
9646
9748
  /**
9647
9749
  * 메인 DB 파일의 물리적 동기화를 수행합니다 (fsync).
@@ -9654,39 +9756,12 @@ var PageMVCCStrategy = class {
9654
9756
  });
9655
9757
  });
9656
9758
  }
9657
- /**
9658
- * 페이지 삭제 (실제로는 캐시에서만 제거)
9659
- * 실제 페이지 해제는 상위 레이어(FreeList)에서 관리합니다.
9660
- * @param pageId 페이지 ID
9661
- */
9662
- async delete(pageId) {
9663
- this.dirtyPages.delete(pageId);
9664
- this.cache.delete(pageId);
9665
- }
9666
- /**
9667
- * 페이지 존재 여부 확인
9668
- * @param pageId 페이지 ID
9669
- * @returns 존재하면 true
9670
- */
9671
- async exists(pageId) {
9672
- if (this.dirtyPages.has(pageId)) {
9673
- return true;
9674
- }
9675
- const pageStartPos = pageId * this.pageSize;
9676
- return pageStartPos < this.fileSize;
9677
- }
9678
9759
  /**
9679
9760
  * 현재 파일 크기 반환
9680
9761
  */
9681
9762
  getFileSize() {
9682
9763
  return this.fileSize;
9683
9764
  }
9684
- /**
9685
- * 캐시 초기화
9686
- */
9687
- clearCache() {
9688
- this.cache.clear();
9689
- }
9690
9765
  // ─────────────────────────────────────────────────────────────
9691
9766
  // Private helpers
9692
9767
  // ─────────────────────────────────────────────────────────────
@@ -9722,13 +9797,15 @@ var PageFileSystem = class {
9722
9797
  const walPath = options.wal;
9723
9798
  this.walManager = walPath && walLogger ? new WALManager(walPath, pageSize, walLogger) : null;
9724
9799
  this.pageManagerFactory = new PageManagerFactory();
9725
- this.pageStrategy = new PageMVCCStrategy(fileHandle, pageSize, pageCacheCapacity);
9800
+ this.pageStrategy = new PageMVCCStrategy(fileHandle, pageSize);
9801
+ this.rootTransaction = new AsyncMVCCTransaction2(this.pageStrategy, { cacheCapacity: pageCacheCapacity });
9726
9802
  this.logger = logger;
9727
9803
  }
9728
9804
  pageFactory = new PageManagerFactory();
9729
9805
  walManager;
9730
9806
  pageManagerFactory;
9731
9807
  pageStrategy;
9808
+ rootTransaction;
9732
9809
  logger;
9733
9810
  /** 글로벌 동기화(체크포인트/커밋)를 위한 Mutex */
9734
9811
  lockPromise = Promise.resolve();
@@ -9769,6 +9846,12 @@ var PageFileSystem = class {
9769
9846
  getPageStrategy() {
9770
9847
  return this.pageStrategy;
9771
9848
  }
9849
+ /**
9850
+ * Returns the root MVCC transaction.
9851
+ */
9852
+ getRootTransaction() {
9853
+ return this.rootTransaction;
9854
+ }
9772
9855
  /**
9773
9856
  * Updates the bitmap status for a specific page.
9774
9857
  * @param pageId The ID of the page to update
@@ -9824,11 +9907,7 @@ var PageFileSystem = class {
9824
9907
  * @returns 페이지 버퍼
9825
9908
  */
9826
9909
  async get(pageIndex, tx) {
9827
- const page = await tx.readPage(pageIndex);
9828
- if (page === null) {
9829
- return new Uint8Array(this.pageSize);
9830
- }
9831
- return page;
9910
+ return tx.__readPage(pageIndex);
9832
9911
  }
9833
9912
  /**
9834
9913
  * Reads the page header.
@@ -9919,7 +9998,7 @@ var PageFileSystem = class {
9919
9998
  const manager = this.pageFactory.getManager(page);
9920
9999
  manager.updateChecksum(page);
9921
10000
  await tx.__acquireWriteLock(pageIndex);
9922
- await tx.writePage(pageIndex, page);
10001
+ await tx.__writePage(pageIndex, page);
9923
10002
  }
9924
10003
  /**
9925
10004
  * Appends and inserts a new page.
@@ -10066,27 +10145,14 @@ var PageFileSystem = class {
10066
10145
  metadataManager.setFreePageId(metadata, pageId);
10067
10146
  await this.setPage(0, metadata, tx);
10068
10147
  }
10069
- /**
10070
- * WAL에 커밋합니다.
10071
- * @param dirtyPages 변경된 페이지들
10072
- */
10073
- async commitToWAL(dirtyPages) {
10074
- if (this.walManager) {
10075
- this.logger.debug(`Committing ${dirtyPages.size} pages to WAL`);
10076
- await this.walManager.prepareCommit(dirtyPages);
10077
- await this.walManager.finalizeCommit(true);
10078
- }
10079
- }
10080
10148
  /**
10081
10149
  * 체크포인트를 수행합니다.
10082
- * 1. 메모리의 더티 페이지를 DB 파일에 기록 (Flush)
10083
- * 2. DB 파일 물리적 동기화 (Sync/fsync)
10084
- * 3. WAL 로그 파일 비우기 (Clear/Truncate)
10150
+ * 1. DB 파일 물리적 동기화 (Sync/fsync)
10151
+ * 2. WAL 로그 파일 비우기 (Clear/Truncate)
10085
10152
  */
10086
10153
  async checkpoint() {
10087
10154
  this.logger.info("Starting checkpoint");
10088
10155
  await this.runGlobalLock(async () => {
10089
- await this.pageStrategy.flush();
10090
10156
  await this.pageStrategy.sync();
10091
10157
  if (this.walManager) {
10092
10158
  await this.walManager.clear();
@@ -10891,17 +10957,16 @@ var Transaction = class {
10891
10957
  /**
10892
10958
  * @param id Transaction ID
10893
10959
  * @param context Transaction context
10894
- * @param pageStrategy Page MVCC Strategy for disk I/O
10960
+ * @param rootTx Root MVCC Transaction
10895
10961
  * @param lockManager LockManager instance
10896
10962
  * @param pfs Page File System
10897
- * @param reloadBPTree Callback to reload BPTree cache on rollback
10898
10963
  */
10899
- constructor(id, context, pageStrategy, lockManager, pfs) {
10964
+ constructor(id, context, rootTx, lockManager, pfs) {
10900
10965
  this.context = context;
10901
10966
  this.lockManager = lockManager;
10902
10967
  this.pfs = pfs;
10903
10968
  this.id = id;
10904
- this.pageStrategy = pageStrategy;
10969
+ this.rootTx = rootTx;
10905
10970
  }
10906
10971
  /** Transaction ID */
10907
10972
  id;
@@ -10909,16 +10974,25 @@ var Transaction = class {
10909
10974
  heldLocks = /* @__PURE__ */ new Set();
10910
10975
  /** Held page locks (PageID -> LockID) */
10911
10976
  pageLocks = /* @__PURE__ */ new Map();
10912
- /** Dirty Pages modified by the transaction: PageID -> Modified Page Buffer */
10913
- dirtyPages = /* @__PURE__ */ new Map();
10914
- /** Undo pages: PageID -> Original Page Buffer (Snapshot) */
10915
- undoPages = /* @__PURE__ */ new Map();
10916
10977
  /** List of callbacks to execute on commit */
10917
10978
  commitHooks = [];
10918
- /** Page MVCC Strategy for disk access */
10919
- pageStrategy;
10979
+ /** Nested MVCC Transaction for snapshot isolation (lazy init) */
10980
+ mvccTx = null;
10981
+ /** Root MVCC Transaction reference */
10982
+ rootTx;
10920
10983
  /** Release function for global write lock, set by DataplyAPI */
10921
10984
  _writeLockRelease = null;
10985
+ /**
10986
+ * Lazily initializes the nested MVCC transaction.
10987
+ * This ensures the snapshot is taken at the time of first access,
10988
+ * picking up the latest committed root version.
10989
+ */
10990
+ ensureMvccTx() {
10991
+ if (!this.mvccTx) {
10992
+ this.mvccTx = this.rootTx.createNested();
10993
+ }
10994
+ return this.mvccTx;
10995
+ }
10922
10996
  /**
10923
10997
  * Registers a commit hook.
10924
10998
  * @param hook Function to execute
@@ -10940,30 +11014,35 @@ var Transaction = class {
10940
11014
  return this._writeLockRelease !== null;
10941
11015
  }
10942
11016
  /**
10943
- * Reads a page. Uses dirty buffer if available, otherwise disk.
11017
+ * Reads a page through the MVCC transaction.
10944
11018
  * @param pageId Page ID
10945
11019
  * @returns Page data
10946
11020
  */
10947
- async readPage(pageId) {
10948
- const dirty = this.dirtyPages.get(pageId);
10949
- if (dirty) {
10950
- return dirty;
11021
+ async __readPage(pageId) {
11022
+ const tx = this.ensureMvccTx();
11023
+ const data = await tx.read(pageId);
11024
+ if (data === null) {
11025
+ return new Uint8Array(this.pfs.pageSize);
10951
11026
  }
10952
- return await this.pageStrategy.read(pageId);
11027
+ const copy = new Uint8Array(data.length);
11028
+ copy.set(data);
11029
+ return copy;
10953
11030
  }
10954
11031
  /**
10955
- * Writes a page to the transaction buffer.
11032
+ * Writes a page through the MVCC transaction.
10956
11033
  * @param pageId Page ID
10957
11034
  * @param data Page data
10958
11035
  */
10959
- async writePage(pageId, data) {
10960
- if (!this.undoPages.has(pageId)) {
10961
- const existingData = await this.pageStrategy.read(pageId);
10962
- const snapshot = new Uint8Array(existingData.length);
10963
- snapshot.set(existingData);
10964
- this.undoPages.set(pageId, snapshot);
11036
+ async __writePage(pageId, data) {
11037
+ const tx = this.ensureMvccTx();
11038
+ const exists = await tx.exists(pageId);
11039
+ if (exists) {
11040
+ const copy = new Uint8Array(data.length);
11041
+ copy.set(data);
11042
+ await tx.write(pageId, copy);
11043
+ } else {
11044
+ await tx.create(pageId, data);
10965
11045
  }
10966
- this.dirtyPages.set(pageId, data);
10967
11046
  }
10968
11047
  /**
10969
11048
  * Acquires a write lock.
@@ -10990,21 +11069,31 @@ var Transaction = class {
10990
11069
  await hook();
10991
11070
  }
10992
11071
  });
11072
+ const tx = this.ensureMvccTx();
10993
11073
  let shouldTriggerCheckpoint = false;
10994
11074
  await this.pfs.runGlobalLock(async () => {
10995
- if (this.pfs.wal && this.dirtyPages.size > 0) {
10996
- await this.pfs.wal.prepareCommit(this.dirtyPages);
11075
+ const entries = tx.getResultEntries();
11076
+ const dirtyPages = /* @__PURE__ */ new Map();
11077
+ for (const entry of [...entries.created, ...entries.updated]) {
11078
+ dirtyPages.set(entry.key, entry.data);
11079
+ }
11080
+ const hasDirtyPages = dirtyPages.size > 0;
11081
+ if (this.pfs.wal && hasDirtyPages) {
11082
+ await this.pfs.wal.prepareCommit(dirtyPages);
10997
11083
  await this.pfs.wal.writeCommitMarker();
10998
11084
  }
10999
- for (const [pageId, data] of this.dirtyPages) {
11000
- await this.pageStrategy.write(pageId, data);
11085
+ await tx.commit();
11086
+ if (hasDirtyPages) {
11087
+ await this.rootTx.commit();
11001
11088
  }
11002
- if (!this.pfs.wal) {
11003
- await this.pfs.strategy.flushPages(this.dirtyPages);
11004
- } else {
11005
- this.pfs.wal.incrementWrittenPages(this.dirtyPages.size);
11006
- if (this.pfs.wal.shouldCheckpoint(this.pfs.options.walCheckpointThreshold)) {
11007
- shouldTriggerCheckpoint = true;
11089
+ if (hasDirtyPages) {
11090
+ if (!this.pfs.wal) {
11091
+ await this.pfs.strategy.sync();
11092
+ } else {
11093
+ this.pfs.wal.incrementWrittenPages(dirtyPages.size);
11094
+ if (this.pfs.wal.shouldCheckpoint(this.pfs.options.walCheckpointThreshold)) {
11095
+ shouldTriggerCheckpoint = true;
11096
+ }
11008
11097
  }
11009
11098
  }
11010
11099
  });
@@ -11012,8 +11101,6 @@ var Transaction = class {
11012
11101
  await this.pfs.checkpoint();
11013
11102
  }
11014
11103
  } finally {
11015
- this.dirtyPages.clear();
11016
- this.undoPages.clear();
11017
11104
  this.releaseAllLocks();
11018
11105
  if (this._writeLockRelease) {
11019
11106
  this._writeLockRelease();
@@ -11026,8 +11113,9 @@ var Transaction = class {
11026
11113
  */
11027
11114
  async rollback() {
11028
11115
  try {
11029
- this.dirtyPages.clear();
11030
- this.undoPages.clear();
11116
+ if (this.mvccTx) {
11117
+ this.mvccTx.rollback();
11118
+ }
11031
11119
  this.releaseAllLocks();
11032
11120
  } finally {
11033
11121
  if (this._writeLockRelease) {
@@ -11036,12 +11124,6 @@ var Transaction = class {
11036
11124
  }
11037
11125
  }
11038
11126
  }
11039
- /**
11040
- * Returns the dirty pages map.
11041
- */
11042
- __getDirtyPages() {
11043
- return this.dirtyPages;
11044
- }
11045
11127
  /**
11046
11128
  * Releases all locks.
11047
11129
  */
@@ -11213,7 +11295,7 @@ var DataplyAPI = class {
11213
11295
  pagePreallocationCount: 1e3,
11214
11296
  wal: null,
11215
11297
  walCheckpointThreshold: 1e3,
11216
- logLevel: 2 /* Info */
11298
+ logLevel: 0 /* None */
11217
11299
  }, options);
11218
11300
  }
11219
11301
  /**
@@ -11334,7 +11416,7 @@ var DataplyAPI = class {
11334
11416
  return new Transaction(
11335
11417
  ++this.txIdCounter,
11336
11418
  this.txContext,
11337
- this.pfs.getPageStrategy(),
11419
+ this.pfs.getRootTransaction(),
11338
11420
  this.lockManager,
11339
11421
  this.pfs
11340
11422
  );
@@ -11354,7 +11436,7 @@ var DataplyAPI = class {
11354
11436
  * Used internally by runWithDefaultWrite.
11355
11437
  * @returns A release function
11356
11438
  */
11357
- acquireWriteLock() {
11439
+ async acquireWriteLock() {
11358
11440
  this.logger.debug("Acquiring write lock");
11359
11441
  const previous = this.writeQueue;
11360
11442
  let release;
@@ -1,5 +1,6 @@
1
1
  import type { IndexPage, MetadataPage, DataplyOptions } from '../types';
2
2
  import type { Transaction } from './transaction/Transaction';
3
+ import { AsyncMVCCTransaction } from 'mvcc-api';
3
4
  import { PageManagerFactory } from './Page';
4
5
  import { WALManager } from './WALManager';
5
6
  import { PageMVCCStrategy } from './PageMVCCStrategy';
@@ -17,6 +18,7 @@ export declare class PageFileSystem {
17
18
  protected readonly walManager: WALManager | null;
18
19
  protected readonly pageManagerFactory: PageManagerFactory;
19
20
  protected readonly pageStrategy: PageMVCCStrategy;
21
+ protected readonly rootTransaction: AsyncMVCCTransaction<PageMVCCStrategy, number, Uint8Array>;
20
22
  protected readonly logger: Logger;
21
23
  /** 글로벌 동기화(체크포인트/커밋)를 위한 Mutex */
22
24
  private lockPromise;
@@ -39,6 +41,10 @@ export declare class PageFileSystem {
39
41
  * Returns the page strategy for transaction use.
40
42
  */
41
43
  getPageStrategy(): PageMVCCStrategy;
44
+ /**
45
+ * Returns the root MVCC transaction.
46
+ */
47
+ getRootTransaction(): AsyncMVCCTransaction<PageMVCCStrategy, number, Uint8Array>;
42
48
  /**
43
49
  * Updates the bitmap status for a specific page.
44
50
  * @param pageId The ID of the page to update
@@ -134,16 +140,10 @@ export declare class PageFileSystem {
134
140
  * @param tx Transaction
135
141
  */
136
142
  setFreePage(pageId: number, tx: Transaction): Promise<void>;
137
- /**
138
- * WAL에 커밋합니다.
139
- * @param dirtyPages 변경된 페이지들
140
- */
141
- commitToWAL(dirtyPages: Map<number, Uint8Array>): Promise<void>;
142
143
  /**
143
144
  * 체크포인트를 수행합니다.
144
- * 1. 메모리의 더티 페이지를 DB 파일에 기록 (Flush)
145
- * 2. DB 파일 물리적 동기화 (Sync/fsync)
146
- * 3. WAL 로그 파일 비우기 (Clear/Truncate)
145
+ * 1. DB 파일 물리적 동기화 (Sync/fsync)
146
+ * 2. WAL 로그 파일 비우기 (Clear/Truncate)
147
147
  */
148
148
  checkpoint(): Promise<void>;
149
149
  /**
@@ -1,23 +1,20 @@
1
+ import { AsyncMVCCStrategy } from 'mvcc-api';
1
2
  /**
2
3
  * 페이지 수준 MVCC Strategy.
3
4
  * mvcc-api의 AsyncMVCCStrategy를 상속하여 디스크 I/O를 담당합니다.
5
+ * 캐시 및 버퍼 관리는 mvcc-api의 AsyncMVCCTransaction이 담당합니다.
4
6
  *
5
7
  * 키: 페이지 ID (number)
6
8
  * 값: 페이지 데이터 (Uint8Array)
7
9
  */
8
- export declare class PageMVCCStrategy {
10
+ export declare class PageMVCCStrategy extends AsyncMVCCStrategy<number, Uint8Array> {
9
11
  private readonly fileHandle;
10
12
  private readonly pageSize;
11
- /** LRU 캐시 (페이지 ID -> 페이지 데이터) */
12
- private readonly cache;
13
- /** 디스크에 기록되지 않은 변경된 페이지들 (페이지 ID -> 페이지 데이터) */
14
- private readonly dirtyPages;
15
13
  /** 파일 크기 (논리적) */
16
14
  private fileSize;
17
- constructor(fileHandle: number, pageSize: number, cacheCapacity: number);
15
+ constructor(fileHandle: number, pageSize: number);
18
16
  /**
19
17
  * 디스크에서 페이지를 읽습니다.
20
- * 캐시에 있으면 캐시에서 반환합니다.
21
18
  * @param pageId 페이지 ID
22
19
  * @returns 페이지 데이터
23
20
  */
@@ -29,22 +26,7 @@ export declare class PageMVCCStrategy {
29
26
  */
30
27
  write(pageId: number, data: Uint8Array): Promise<void>;
31
28
  /**
32
- * 더티 페이지들을 메인 디스크 파일에 일괄 기록합니다.
33
- * WAL 체크포인트 시점에 호출되어야 합니다.
34
- */
35
- flush(): Promise<void>;
36
- /**
37
- * 지정된 페이지들만 디스크에 기록합니다.
38
- * WAL 없이 트랜잭션 커밋 시, 해당 트랜잭션의 dirty pages만 선택적으로 flush합니다.
39
- * @param pages 기록할 페이지 맵 (PageID -> PageData)
40
- */
41
- flushPages(pages: Map<number, Uint8Array>): Promise<void>;
42
- /**
43
- * 메인 DB 파일의 물리적 동기화를 수행합니다 (fsync).
44
- */
45
- sync(): Promise<void>;
46
- /**
47
- * 페이지 삭제 (실제로는 캐시에서만 제거)
29
+ * 페이지 삭제.
48
30
  * 실제 페이지 해제는 상위 레이어(FreeList)에서 관리합니다.
49
31
  * @param pageId 페이지 ID
50
32
  */
@@ -56,13 +38,13 @@ export declare class PageMVCCStrategy {
56
38
  */
57
39
  exists(pageId: number): Promise<boolean>;
58
40
  /**
59
- * 현재 파일 크기 반환
41
+ * 메인 DB 파일의 물리적 동기화를 수행합니다 (fsync).
60
42
  */
61
- getFileSize(): number;
43
+ sync(): Promise<void>;
62
44
  /**
63
- * 캐시 초기화
45
+ * 현재 파일 크기 반환
64
46
  */
65
- clearCache(): void;
47
+ getFileSize(): number;
66
48
  private _readFromDisk;
67
49
  private _writeToDisk;
68
50
  }
@@ -1,10 +1,12 @@
1
1
  import type { PageFileSystem } from '../PageFileSystem';
2
+ import type { AsyncMVCCTransaction } from 'mvcc-api';
3
+ import type { PageMVCCStrategy } from '../PageMVCCStrategy';
2
4
  import { LockManager } from './LockManager';
3
5
  import { TransactionContext } from './TxContext';
4
- import { PageMVCCStrategy } from '../PageMVCCStrategy';
5
6
  /**
6
7
  * Transaction class.
7
8
  * Manages the lifecycle and resources of a database transaction.
9
+ * Internally wraps a nested AsyncMVCCTransaction for snapshot isolation.
8
10
  */
9
11
  export declare class Transaction {
10
12
  readonly context: TransactionContext;
@@ -16,25 +18,28 @@ export declare class Transaction {
16
18
  private heldLocks;
17
19
  /** Held page locks (PageID -> LockID) */
18
20
  private pageLocks;
19
- /** Dirty Pages modified by the transaction: PageID -> Modified Page Buffer */
20
- private dirtyPages;
21
- /** Undo pages: PageID -> Original Page Buffer (Snapshot) */
22
- private undoPages;
23
21
  /** List of callbacks to execute on commit */
24
22
  private commitHooks;
25
- /** Page MVCC Strategy for disk access */
26
- private readonly pageStrategy;
23
+ /** Nested MVCC Transaction for snapshot isolation (lazy init) */
24
+ private mvccTx;
25
+ /** Root MVCC Transaction reference */
26
+ private readonly rootTx;
27
27
  /** Release function for global write lock, set by DataplyAPI */
28
28
  private _writeLockRelease;
29
29
  /**
30
30
  * @param id Transaction ID
31
31
  * @param context Transaction context
32
- * @param pageStrategy Page MVCC Strategy for disk I/O
32
+ * @param rootTx Root MVCC Transaction
33
33
  * @param lockManager LockManager instance
34
34
  * @param pfs Page File System
35
- * @param reloadBPTree Callback to reload BPTree cache on rollback
36
35
  */
37
- constructor(id: number, context: TransactionContext, pageStrategy: PageMVCCStrategy, lockManager: LockManager, pfs: PageFileSystem);
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;
38
43
  /**
39
44
  * Registers a commit hook.
40
45
  * @param hook Function to execute
@@ -50,17 +55,17 @@ export declare class Transaction {
50
55
  */
51
56
  __hasWriteLockRelease(): boolean;
52
57
  /**
53
- * Reads a page. Uses dirty buffer if available, otherwise disk.
58
+ * Reads a page through the MVCC transaction.
54
59
  * @param pageId Page ID
55
60
  * @returns Page data
56
61
  */
57
- readPage(pageId: number): Promise<Uint8Array>;
62
+ __readPage(pageId: number): Promise<Uint8Array>;
58
63
  /**
59
- * Writes a page to the transaction buffer.
64
+ * Writes a page through the MVCC transaction.
60
65
  * @param pageId Page ID
61
66
  * @param data Page data
62
67
  */
63
- writePage(pageId: number, data: Uint8Array): Promise<void>;
68
+ __writePage(pageId: number, data: Uint8Array): Promise<void>;
64
69
  /**
65
70
  * Acquires a write lock.
66
71
  * @param pageId Page ID
@@ -74,10 +79,6 @@ export declare class Transaction {
74
79
  * Rolls back the transaction.
75
80
  */
76
81
  rollback(): Promise<void>;
77
- /**
78
- * Returns the dirty pages map.
79
- */
80
- __getDirtyPages(): Map<number, Uint8Array>;
81
82
  /**
82
83
  * Releases all locks.
83
84
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dataply",
3
- "version": "0.0.26-alpha.5",
3
+ "version": "0.0.26-alpha.7",
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
+ }