dataply 0.0.20-alpha.2 → 0.0.20-alpha.4

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,50 @@ 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 snapshot = new Map(this.dirtyPages);
7652
+ const sortedPageIds = Array.from(snapshot.keys()).sort((a, b) => a - b);
7653
+ for (const pageId of sortedPageIds) {
7654
+ const data = snapshot.get(pageId);
7655
+ const position = pageId * this.pageSize;
7656
+ await this._writeToDisk(data, position);
7657
+ this.dirtyPages.delete(pageId);
7658
+ }
7659
+ }
7660
+ /**
7661
+ * 메인 DB 파일의 물리적 동기화를 수행합니다 (fsync).
7662
+ */
7663
+ async sync() {
7664
+ return new Promise((resolve, reject) => {
7665
+ import_node_fs2.default.fsync(this.fileHandle, (err) => {
7666
+ if (err) return reject(err);
7667
+ resolve();
7668
+ });
7669
+ });
7670
+ }
7638
7671
  /**
7639
7672
  * 페이지 삭제 (실제로는 캐시에서만 제거)
7640
7673
  * 실제 페이지 해제는 상위 레이어(FreeList)에서 관리합니다.
7641
7674
  * @param pageId 페이지 ID
7642
7675
  */
7643
7676
  async delete(pageId) {
7677
+ this.dirtyPages.delete(pageId);
7644
7678
  this.cache.delete(pageId);
7645
7679
  }
7646
7680
  /**
@@ -7649,6 +7683,9 @@ var PageMVCCStrategy = class extends AsyncMVCCStrategy2 {
7649
7683
  * @returns 존재하면 true
7650
7684
  */
7651
7685
  async exists(pageId) {
7686
+ if (this.dirtyPages.has(pageId)) {
7687
+ return true;
7688
+ }
7652
7689
  const pageStartPos = pageId * this.pageSize;
7653
7690
  return pageStartPos < this.fileSize;
7654
7691
  }
@@ -7705,6 +7742,25 @@ var PageFileSystem = class {
7705
7742
  walManager;
7706
7743
  pageManagerFactory;
7707
7744
  pageStrategy;
7745
+ /** 글로벌 동기화(체크포인트/커밋)를 위한 Mutex */
7746
+ lockPromise = Promise.resolve();
7747
+ /**
7748
+ * 글로벌 동기화 범위 내에서 작업을 실행합니다.
7749
+ * @param task 수행할 비동기 작업
7750
+ */
7751
+ async runGlobalLock(task) {
7752
+ const previous = this.lockPromise;
7753
+ let resolveLock;
7754
+ this.lockPromise = new Promise((resolve) => {
7755
+ resolveLock = resolve;
7756
+ });
7757
+ await previous;
7758
+ try {
7759
+ return await task();
7760
+ } finally {
7761
+ resolveLock();
7762
+ }
7763
+ }
7708
7764
  /**
7709
7765
  * Initializes the page file system.
7710
7766
  * Performs WAL recovery if necessary.
@@ -7714,6 +7770,7 @@ var PageFileSystem = class {
7714
7770
  await this.walManager.recover(async (pageId, data) => {
7715
7771
  await this.pageStrategy.write(pageId, data);
7716
7772
  });
7773
+ await this.checkpoint();
7717
7774
  }
7718
7775
  }
7719
7776
  /**
@@ -8013,15 +8070,30 @@ var PageFileSystem = class {
8013
8070
  async commitToWAL(dirtyPages) {
8014
8071
  if (this.walManager) {
8015
8072
  await this.walManager.prepareCommit(dirtyPages);
8016
- await this.walManager.finalizeCommit(false);
8073
+ await this.walManager.finalizeCommit(true);
8017
8074
  }
8018
8075
  }
8076
+ /**
8077
+ * 체크포인트를 수행합니다.
8078
+ * 1. 메모리의 더티 페이지를 DB 파일에 기록 (Flush)
8079
+ * 2. DB 파일 물리적 동기화 (Sync/fsync)
8080
+ * 3. WAL 로그 파일 비우기 (Clear/Truncate)
8081
+ */
8082
+ async checkpoint() {
8083
+ await this.runGlobalLock(async () => {
8084
+ await this.pageStrategy.flush();
8085
+ await this.pageStrategy.sync();
8086
+ if (this.walManager) {
8087
+ await this.walManager.clear();
8088
+ }
8089
+ });
8090
+ }
8019
8091
  /**
8020
8092
  * Closes the page file system.
8021
8093
  */
8022
8094
  async close() {
8095
+ await this.checkpoint();
8023
8096
  if (this.walManager) {
8024
- await this.walManager.clear();
8025
8097
  this.walManager.close();
8026
8098
  }
8027
8099
  }
@@ -8847,18 +8919,24 @@ var Transaction = class {
8847
8919
  await hook();
8848
8920
  }
8849
8921
  });
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();
8922
+ let shouldTriggerCheckpoint = false;
8923
+ await this.pfs.runGlobalLock(async () => {
8924
+ if (this.pfs.wal && this.dirtyPages.size > 0) {
8925
+ await this.pfs.wal.prepareCommit(this.dirtyPages);
8926
+ await this.pfs.wal.writeCommitMarker();
8861
8927
  }
8928
+ for (const [pageId, data] of this.dirtyPages) {
8929
+ await this.pageStrategy.write(pageId, data);
8930
+ }
8931
+ if (this.pfs.wal) {
8932
+ this.pfs.wal.incrementWrittenPages(this.dirtyPages.size);
8933
+ if (this.pfs.wal.shouldCheckpoint(this.pfs.options.walCheckpointThreshold)) {
8934
+ shouldTriggerCheckpoint = true;
8935
+ }
8936
+ }
8937
+ });
8938
+ if (shouldTriggerCheckpoint) {
8939
+ await this.pfs.checkpoint();
8862
8940
  }
8863
8941
  this.dirtyPages.clear();
8864
8942
  this.undoPages.clear();
@@ -16,11 +16,18 @@ export declare class PageFileSystem {
16
16
  protected readonly walManager: WALManager | null;
17
17
  protected readonly pageManagerFactory: PageManagerFactory;
18
18
  protected readonly pageStrategy: PageMVCCStrategy;
19
+ /** 글로벌 동기화(체크포인트/커밋)를 위한 Mutex */
20
+ private lockPromise;
19
21
  /**
20
22
  * @param pageCacheCapacity 페이지 캐시 크기
21
23
  * @param options 데이터플라이 옵션
22
24
  */
23
25
  constructor(fileHandle: number, pageSize: number, pageCacheCapacity: number, options: Required<DataplyOptions>);
26
+ /**
27
+ * 글로벌 동기화 범위 내에서 작업을 실행합니다.
28
+ * @param task 수행할 비동기 작업
29
+ */
30
+ runGlobalLock<T>(task: () => Promise<T>): Promise<T>;
24
31
  /**
25
32
  * Initializes the page file system.
26
33
  * Performs WAL recovery if necessary.
@@ -130,6 +137,13 @@ export declare class PageFileSystem {
130
137
  * @param dirtyPages 변경된 페이지들
131
138
  */
132
139
  commitToWAL(dirtyPages: Map<number, Uint8Array>): Promise<void>;
140
+ /**
141
+ * 체크포인트를 수행합니다.
142
+ * 1. 메모리의 더티 페이지를 DB 파일에 기록 (Flush)
143
+ * 2. DB 파일 물리적 동기화 (Sync/fsync)
144
+ * 3. WAL 로그 파일 비우기 (Clear/Truncate)
145
+ */
146
+ checkpoint(): Promise<void>;
133
147
  /**
134
148
  * Closes the page file system.
135
149
  */
@@ -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.4",
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>",