dataply 0.0.20-alpha.2 → 0.0.20-alpha.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.js CHANGED
@@ -7427,9 +7427,6 @@ var WALManager = class {
7427
7427
  promises.push(writePage(pageId, data));
7428
7428
  }
7429
7429
  await Promise.all(promises);
7430
- if (restoredPages.size > 0) {
7431
- await this.clear();
7432
- }
7433
7430
  }
7434
7431
  /**
7435
7432
  * WAL에 페이지 데이터를 기록합니다 (Phase 1: Prepare).
@@ -7590,6 +7587,8 @@ var PageMVCCStrategy = class extends AsyncMVCCStrategy2 {
7590
7587
  }
7591
7588
  /** LRU 캐시 (페이지 ID -> 페이지 데이터) */
7592
7589
  cache;
7590
+ /** 디스크에 기록되지 않은 변경된 페이지들 (페이지 ID -> 페이지 데이터) */
7591
+ dirtyPages = /* @__PURE__ */ new Map();
7593
7592
  /** 파일 크기 (논리적) */
7594
7593
  fileSize;
7595
7594
  /**
@@ -7599,6 +7598,12 @@ var PageMVCCStrategy = class extends AsyncMVCCStrategy2 {
7599
7598
  * @returns 페이지 데이터
7600
7599
  */
7601
7600
  async read(pageId) {
7601
+ const dirty = this.dirtyPages.get(pageId);
7602
+ if (dirty) {
7603
+ const copy = new Uint8Array(this.pageSize);
7604
+ copy.set(dirty);
7605
+ return copy;
7606
+ }
7602
7607
  const cached = this.cache.get(pageId);
7603
7608
  if (cached) {
7604
7609
  const copy = new Uint8Array(this.pageSize);
@@ -7626,21 +7631,49 @@ var PageMVCCStrategy = class extends AsyncMVCCStrategy2 {
7626
7631
  if (pageStartPos + this.pageSize > 512 * 1024 * 1024) {
7627
7632
  throw new Error(`[Safety Limit] File write exceeds 512MB limit at position ${pageStartPos}`);
7628
7633
  }
7629
- await this._writeToDisk(data, pageStartPos);
7630
- const cacheCopy = new Uint8Array(this.pageSize);
7631
- cacheCopy.set(data);
7632
- this.cache.set(pageId, cacheCopy);
7634
+ const dataCopy = new Uint8Array(this.pageSize);
7635
+ dataCopy.set(data);
7636
+ this.dirtyPages.set(pageId, dataCopy);
7637
+ this.cache.set(pageId, dataCopy);
7633
7638
  const endPosition = pageStartPos + this.pageSize;
7634
7639
  if (endPosition > this.fileSize) {
7635
7640
  this.fileSize = endPosition;
7636
7641
  }
7637
7642
  }
7643
+ /**
7644
+ * 더티 페이지들을 메인 디스크 파일에 일괄 기록합니다.
7645
+ * WAL 체크포인트 시점에 호출되어야 합니다.
7646
+ */
7647
+ async flush() {
7648
+ if (this.dirtyPages.size === 0) {
7649
+ return;
7650
+ }
7651
+ const sortedPageIds = Array.from(this.dirtyPages.keys()).sort((a, b) => a - b);
7652
+ for (const pageId of sortedPageIds) {
7653
+ const data = this.dirtyPages.get(pageId);
7654
+ const position = pageId * this.pageSize;
7655
+ await this._writeToDisk(data, position);
7656
+ }
7657
+ this.dirtyPages.clear();
7658
+ }
7659
+ /**
7660
+ * 메인 DB 파일의 물리적 동기화를 수행합니다 (fsync).
7661
+ */
7662
+ async sync() {
7663
+ return new Promise((resolve, reject) => {
7664
+ import_node_fs2.default.fsync(this.fileHandle, (err) => {
7665
+ if (err) return reject(err);
7666
+ resolve();
7667
+ });
7668
+ });
7669
+ }
7638
7670
  /**
7639
7671
  * 페이지 삭제 (실제로는 캐시에서만 제거)
7640
7672
  * 실제 페이지 해제는 상위 레이어(FreeList)에서 관리합니다.
7641
7673
  * @param pageId 페이지 ID
7642
7674
  */
7643
7675
  async delete(pageId) {
7676
+ this.dirtyPages.delete(pageId);
7644
7677
  this.cache.delete(pageId);
7645
7678
  }
7646
7679
  /**
@@ -7649,6 +7682,9 @@ var PageMVCCStrategy = class extends AsyncMVCCStrategy2 {
7649
7682
  * @returns 존재하면 true
7650
7683
  */
7651
7684
  async exists(pageId) {
7685
+ if (this.dirtyPages.has(pageId)) {
7686
+ return true;
7687
+ }
7652
7688
  const pageStartPos = pageId * this.pageSize;
7653
7689
  return pageStartPos < this.fileSize;
7654
7690
  }
@@ -7700,11 +7736,13 @@ var PageFileSystem = class {
7700
7736
  this.walManager = walPath ? new WALManager(walPath, pageSize) : null;
7701
7737
  this.pageManagerFactory = new PageManagerFactory();
7702
7738
  this.pageStrategy = new PageMVCCStrategy(fileHandle, pageSize, pageCacheCapacity);
7739
+ this.lock = new Ryoiki3();
7703
7740
  }
7704
7741
  pageFactory = new PageManagerFactory();
7705
7742
  walManager;
7706
7743
  pageManagerFactory;
7707
7744
  pageStrategy;
7745
+ lock;
7708
7746
  /**
7709
7747
  * Initializes the page file system.
7710
7748
  * Performs WAL recovery if necessary.
@@ -7714,8 +7752,18 @@ var PageFileSystem = class {
7714
7752
  await this.walManager.recover(async (pageId, data) => {
7715
7753
  await this.pageStrategy.write(pageId, data);
7716
7754
  });
7755
+ await this.checkpoint();
7717
7756
  }
7718
7757
  }
7758
+ async lockCheckpoint(fn) {
7759
+ let lockId;
7760
+ return this.lock.writeLock(async (_lockId) => {
7761
+ lockId = _lockId;
7762
+ await fn();
7763
+ }).finally(() => {
7764
+ this.lock.writeUnlock(lockId);
7765
+ });
7766
+ }
7719
7767
  /**
7720
7768
  * Returns the page strategy for transaction use.
7721
7769
  */
@@ -8013,15 +8061,30 @@ var PageFileSystem = class {
8013
8061
  async commitToWAL(dirtyPages) {
8014
8062
  if (this.walManager) {
8015
8063
  await this.walManager.prepareCommit(dirtyPages);
8016
- await this.walManager.finalizeCommit(false);
8064
+ await this.walManager.finalizeCommit(true);
8017
8065
  }
8018
8066
  }
8067
+ /**
8068
+ * 체크포인트를 수행합니다.
8069
+ * 1. 메모리의 더티 페이지를 DB 파일에 기록 (Flush)
8070
+ * 2. DB 파일 물리적 동기화 (Sync/fsync)
8071
+ * 3. WAL 로그 파일 비우기 (Clear/Truncate)
8072
+ */
8073
+ async checkpoint() {
8074
+ return this.lockCheckpoint(async () => {
8075
+ await this.pageStrategy.flush();
8076
+ await this.pageStrategy.sync();
8077
+ if (this.walManager) {
8078
+ await this.walManager.clear();
8079
+ }
8080
+ });
8081
+ }
8019
8082
  /**
8020
8083
  * Closes the page file system.
8021
8084
  */
8022
8085
  async close() {
8086
+ await this.checkpoint();
8023
8087
  if (this.walManager) {
8024
- await this.walManager.clear();
8025
8088
  this.walManager.close();
8026
8089
  }
8027
8090
  }
@@ -8842,27 +8905,29 @@ var Transaction = class {
8842
8905
  * Commits the transaction.
8843
8906
  */
8844
8907
  async commit() {
8845
- await this.context.run(this, async () => {
8846
- for (const hook of this.commitHooks) {
8847
- await hook();
8908
+ return this.pfs.lockCheckpoint(async () => {
8909
+ await this.context.run(this, async () => {
8910
+ for (const hook of this.commitHooks) {
8911
+ await hook();
8912
+ }
8913
+ });
8914
+ if (this.pfs.wal && this.dirtyPages.size > 0) {
8915
+ await this.pfs.wal.prepareCommit(this.dirtyPages);
8916
+ await this.pfs.wal.writeCommitMarker();
8848
8917
  }
8849
- });
8850
- if (this.pfs.wal && this.dirtyPages.size > 0) {
8851
- await this.pfs.wal.prepareCommit(this.dirtyPages);
8852
- await this.pfs.wal.writeCommitMarker();
8853
- }
8854
- for (const [pageId, data] of this.dirtyPages) {
8855
- await this.pageStrategy.write(pageId, data);
8856
- }
8857
- if (this.pfs.wal) {
8858
- this.pfs.wal.incrementWrittenPages(this.dirtyPages.size);
8859
- if (this.pfs.wal.shouldCheckpoint(this.pfs.options.walCheckpointThreshold)) {
8860
- await this.pfs.wal.clear();
8918
+ for (const [pageId, data] of this.dirtyPages) {
8919
+ await this.pageStrategy.write(pageId, data);
8861
8920
  }
8862
- }
8863
- this.dirtyPages.clear();
8864
- this.undoPages.clear();
8865
- this.releaseAllLocks();
8921
+ if (this.pfs.wal) {
8922
+ this.pfs.wal.incrementWrittenPages(this.dirtyPages.size);
8923
+ if (this.pfs.wal.shouldCheckpoint(this.pfs.options.walCheckpointThreshold)) {
8924
+ await this.pfs.checkpoint();
8925
+ }
8926
+ }
8927
+ this.dirtyPages.clear();
8928
+ this.undoPages.clear();
8929
+ this.releaseAllLocks();
8930
+ });
8866
8931
  }
8867
8932
  /**
8868
8933
  * Rolls back the transaction.
@@ -1,4 +1,5 @@
1
1
  import type { IndexPage, MetadataPage, DataplyOptions } from '../types';
2
+ import { Ryoiki } from 'ryoiki';
2
3
  import type { Transaction } from './transaction/Transaction';
3
4
  import { PageManagerFactory } from './Page';
4
5
  import { WALManager } from './WALManager';
@@ -16,6 +17,7 @@ export declare class PageFileSystem {
16
17
  protected readonly walManager: WALManager | null;
17
18
  protected readonly pageManagerFactory: PageManagerFactory;
18
19
  protected readonly pageStrategy: PageMVCCStrategy;
20
+ protected readonly lock: Ryoiki;
19
21
  /**
20
22
  * @param pageCacheCapacity 페이지 캐시 크기
21
23
  * @param options 데이터플라이 옵션
@@ -26,6 +28,7 @@ export declare class PageFileSystem {
26
28
  * Performs WAL recovery if necessary.
27
29
  */
28
30
  init(): Promise<void>;
31
+ lockCheckpoint(fn: () => Promise<void>): Promise<void>;
29
32
  /**
30
33
  * Returns the page strategy for transaction use.
31
34
  */
@@ -130,6 +133,13 @@ export declare class PageFileSystem {
130
133
  * @param dirtyPages 변경된 페이지들
131
134
  */
132
135
  commitToWAL(dirtyPages: Map<number, Uint8Array>): Promise<void>;
136
+ /**
137
+ * 체크포인트를 수행합니다.
138
+ * 1. 메모리의 더티 페이지를 DB 파일에 기록 (Flush)
139
+ * 2. DB 파일 물리적 동기화 (Sync/fsync)
140
+ * 3. WAL 로그 파일 비우기 (Clear/Truncate)
141
+ */
142
+ checkpoint(): Promise<void>;
133
143
  /**
134
144
  * Closes the page file system.
135
145
  */
@@ -11,6 +11,8 @@ export declare class PageMVCCStrategy extends AsyncMVCCStrategy<number, Uint8Arr
11
11
  private readonly pageSize;
12
12
  /** LRU 캐시 (페이지 ID -> 페이지 데이터) */
13
13
  private readonly cache;
14
+ /** 디스크에 기록되지 않은 변경된 페이지들 (페이지 ID -> 페이지 데이터) */
15
+ private readonly dirtyPages;
14
16
  /** 파일 크기 (논리적) */
15
17
  private fileSize;
16
18
  constructor(fileHandle: number, pageSize: number, cacheCapacity: number);
@@ -27,6 +29,15 @@ export declare class PageMVCCStrategy extends AsyncMVCCStrategy<number, Uint8Arr
27
29
  * @param data 페이지 데이터
28
30
  */
29
31
  write(pageId: number, data: Uint8Array): Promise<void>;
32
+ /**
33
+ * 더티 페이지들을 메인 디스크 파일에 일괄 기록합니다.
34
+ * WAL 체크포인트 시점에 호출되어야 합니다.
35
+ */
36
+ flush(): Promise<void>;
37
+ /**
38
+ * 메인 DB 파일의 물리적 동기화를 수행합니다 (fsync).
39
+ */
40
+ sync(): Promise<void>;
30
41
  /**
31
42
  * 페이지 삭제 (실제로는 캐시에서만 제거)
32
43
  * 실제 페이지 해제는 상위 레이어(FreeList)에서 관리합니다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dataply",
3
- "version": "0.0.20-alpha.2",
3
+ "version": "0.0.20-alpha.3",
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>",