document-dataply 0.0.4-alpha.3 → 0.0.4-alpha.5

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.
Files changed (2) hide show
  1. package/dist/cjs/index.js +197 -40
  2. package/package.json +3 -3
package/dist/cjs/index.js CHANGED
@@ -7434,9 +7434,6 @@ var require_cjs = __commonJS({
7434
7434
  promises.push(writePage(pageId, data));
7435
7435
  }
7436
7436
  await Promise.all(promises);
7437
- if (restoredPages.size > 0) {
7438
- await this.clear();
7439
- }
7440
7437
  }
7441
7438
  /**
7442
7439
  * WAL에 페이지 데이터를 기록합니다 (Phase 1: Prepare).
@@ -7595,6 +7592,8 @@ var require_cjs = __commonJS({
7595
7592
  }
7596
7593
  /** LRU 캐시 (페이지 ID -> 페이지 데이터) */
7597
7594
  cache;
7595
+ /** 디스크에 기록되지 않은 변경된 페이지들 (페이지 ID -> 페이지 데이터) */
7596
+ dirtyPages = /* @__PURE__ */ new Map();
7598
7597
  /** 파일 크기 (논리적) */
7599
7598
  fileSize;
7600
7599
  /**
@@ -7604,12 +7603,16 @@ var require_cjs = __commonJS({
7604
7603
  * @returns 페이지 데이터
7605
7604
  */
7606
7605
  async read(pageId) {
7607
- const cached = this.cache.get(pageId);
7608
- if (cached) {
7606
+ const dirty = this.dirtyPages.get(pageId);
7607
+ if (dirty) {
7609
7608
  const copy = new Uint8Array(this.pageSize);
7610
- copy.set(cached);
7609
+ copy.set(dirty);
7611
7610
  return copy;
7612
7611
  }
7612
+ const cached = this.cache.get(pageId);
7613
+ if (cached) {
7614
+ return cached;
7615
+ }
7613
7616
  const buffer = new Uint8Array(this.pageSize);
7614
7617
  const pageStartPos = pageId * this.pageSize;
7615
7618
  if (pageStartPos >= this.fileSize) {
@@ -7631,21 +7634,50 @@ var require_cjs = __commonJS({
7631
7634
  if (pageStartPos + this.pageSize > 512 * 1024 * 1024) {
7632
7635
  throw new Error(`[Safety Limit] File write exceeds 512MB limit at position ${pageStartPos}`);
7633
7636
  }
7634
- await this._writeToDisk(data, pageStartPos);
7635
- const cacheCopy = new Uint8Array(this.pageSize);
7636
- cacheCopy.set(data);
7637
- this.cache.set(pageId, cacheCopy);
7637
+ const dataCopy = new Uint8Array(this.pageSize);
7638
+ dataCopy.set(data);
7639
+ this.dirtyPages.set(pageId, dataCopy);
7640
+ this.cache.set(pageId, dataCopy);
7638
7641
  const endPosition = pageStartPos + this.pageSize;
7639
7642
  if (endPosition > this.fileSize) {
7640
7643
  this.fileSize = endPosition;
7641
7644
  }
7642
7645
  }
7646
+ /**
7647
+ * 더티 페이지들을 메인 디스크 파일에 일괄 기록합니다.
7648
+ * WAL 체크포인트 시점에 호출되어야 합니다.
7649
+ */
7650
+ async flush() {
7651
+ if (this.dirtyPages.size === 0) {
7652
+ return;
7653
+ }
7654
+ const snapshot = new Map(this.dirtyPages);
7655
+ const sortedPageIds = Array.from(snapshot.keys()).sort((a, b) => a - b);
7656
+ for (const pageId of sortedPageIds) {
7657
+ const data = snapshot.get(pageId);
7658
+ const position = pageId * this.pageSize;
7659
+ await this._writeToDisk(data, position);
7660
+ this.dirtyPages.delete(pageId);
7661
+ }
7662
+ }
7663
+ /**
7664
+ * 메인 DB 파일의 물리적 동기화를 수행합니다 (fsync).
7665
+ */
7666
+ async sync() {
7667
+ return new Promise((resolve, reject) => {
7668
+ import_node_fs2.default.fsync(this.fileHandle, (err) => {
7669
+ if (err) return reject(err);
7670
+ resolve();
7671
+ });
7672
+ });
7673
+ }
7643
7674
  /**
7644
7675
  * 페이지 삭제 (실제로는 캐시에서만 제거)
7645
7676
  * 실제 페이지 해제는 상위 레이어(FreeList)에서 관리합니다.
7646
7677
  * @param pageId 페이지 ID
7647
7678
  */
7648
7679
  async delete(pageId) {
7680
+ this.dirtyPages.delete(pageId);
7649
7681
  this.cache.delete(pageId);
7650
7682
  }
7651
7683
  /**
@@ -7654,6 +7686,9 @@ var require_cjs = __commonJS({
7654
7686
  * @returns 존재하면 true
7655
7687
  */
7656
7688
  async exists(pageId) {
7689
+ if (this.dirtyPages.has(pageId)) {
7690
+ return true;
7691
+ }
7657
7692
  const pageStartPos = pageId * this.pageSize;
7658
7693
  return pageStartPos < this.fileSize;
7659
7694
  }
@@ -7708,6 +7743,25 @@ var require_cjs = __commonJS({
7708
7743
  walManager;
7709
7744
  pageManagerFactory;
7710
7745
  pageStrategy;
7746
+ /** 글로벌 동기화(체크포인트/커밋)를 위한 Mutex */
7747
+ lockPromise = Promise.resolve();
7748
+ /**
7749
+ * 글로벌 동기화 범위 내에서 작업을 실행합니다.
7750
+ * @param task 수행할 비동기 작업
7751
+ */
7752
+ async runGlobalLock(task) {
7753
+ const previous = this.lockPromise;
7754
+ let resolveLock;
7755
+ this.lockPromise = new Promise((resolve) => {
7756
+ resolveLock = resolve;
7757
+ });
7758
+ await previous;
7759
+ try {
7760
+ return await task();
7761
+ } finally {
7762
+ resolveLock();
7763
+ }
7764
+ }
7711
7765
  /**
7712
7766
  * Initializes the page file system.
7713
7767
  * Performs WAL recovery if necessary.
@@ -7717,6 +7771,7 @@ var require_cjs = __commonJS({
7717
7771
  await this.walManager.recover(async (pageId, data) => {
7718
7772
  await this.pageStrategy.write(pageId, data);
7719
7773
  });
7774
+ await this.checkpoint();
7720
7775
  }
7721
7776
  }
7722
7777
  /**
@@ -8016,19 +8071,35 @@ var require_cjs = __commonJS({
8016
8071
  async commitToWAL(dirtyPages) {
8017
8072
  if (this.walManager) {
8018
8073
  await this.walManager.prepareCommit(dirtyPages);
8019
- await this.walManager.finalizeCommit(false);
8074
+ await this.walManager.finalizeCommit(true);
8020
8075
  }
8021
8076
  }
8077
+ /**
8078
+ * 체크포인트를 수행합니다.
8079
+ * 1. 메모리의 더티 페이지를 DB 파일에 기록 (Flush)
8080
+ * 2. DB 파일 물리적 동기화 (Sync/fsync)
8081
+ * 3. WAL 로그 파일 비우기 (Clear/Truncate)
8082
+ */
8083
+ async checkpoint() {
8084
+ await this.runGlobalLock(async () => {
8085
+ await this.pageStrategy.flush();
8086
+ await this.pageStrategy.sync();
8087
+ if (this.walManager) {
8088
+ await this.walManager.clear();
8089
+ }
8090
+ });
8091
+ }
8022
8092
  /**
8023
8093
  * Closes the page file system.
8024
8094
  */
8025
8095
  async close() {
8096
+ await this.checkpoint();
8026
8097
  if (this.walManager) {
8027
- await this.walManager.clear();
8028
8098
  this.walManager.close();
8029
8099
  }
8030
8100
  }
8031
8101
  };
8102
+ var import_node_os = __toESM2(require("node:os"));
8032
8103
  var TextCodec = class _TextCodec {
8033
8104
  static TextEncoder = new TextEncoder();
8034
8105
  static TextDecoder = new TextDecoder();
@@ -8268,11 +8339,14 @@ var require_cjs = __commonJS({
8268
8339
  this.maxBodySize = this.pfs.pageSize - DataPageManager.CONSTANT.SIZE_PAGE_HEADER;
8269
8340
  this.order = this.getOptimalOrder(pfs.pageSize, IndexPageManager.CONSTANT.SIZE_KEY, IndexPageManager.CONSTANT.SIZE_VALUE);
8270
8341
  this.strategy = new RowIdentifierStrategy(this.order, pfs, txContext);
8342
+ const budget = import_node_os.default.freemem() * 0.05;
8343
+ const nodeMemory = this.order * 24 + 256;
8344
+ const capacity = Math.max(1e3, Math.min(1e6, Math.floor(budget / nodeMemory)));
8271
8345
  this.bptree = new BPTreeAsync2(
8272
8346
  this.strategy,
8273
8347
  new NumericComparator(),
8274
8348
  {
8275
- capacity: this.options.pageCacheCapacity
8349
+ capacity
8276
8350
  }
8277
8351
  );
8278
8352
  }
@@ -8639,6 +8713,30 @@ var require_cjs = __commonJS({
8639
8713
  }
8640
8714
  return this.fetchRowByRid(pk, rid, tx);
8641
8715
  }
8716
+ /**
8717
+ * Selects multiple rows by their PKs in a single B+ Tree traversal.
8718
+ * @param pks Array of PKs to look up
8719
+ * @param tx Transaction
8720
+ * @returns Array of raw data of the rows in the same order as input PKs
8721
+ */
8722
+ async selectMany(pks, tx) {
8723
+ if (pks.length === 0) {
8724
+ return [];
8725
+ }
8726
+ const minPk = Math.min(...pks);
8727
+ const maxPk = Math.max(...pks);
8728
+ const pkSet = new Set(pks);
8729
+ const resultMap = /* @__PURE__ */ new Map();
8730
+ const btx = await this.getBPTreeTransaction(tx);
8731
+ const stream = btx.whereStream({ gte: minPk, lte: maxPk });
8732
+ for await (const [rid, pk] of stream) {
8733
+ if (pkSet.has(pk)) {
8734
+ const rowData = await this.fetchRowByRid(pk, rid, tx);
8735
+ resultMap.set(pk, rowData);
8736
+ }
8737
+ }
8738
+ return pks.map((pk) => resultMap.get(pk) ?? null);
8739
+ }
8642
8740
  async fetchRowByRid(pk, rid, tx) {
8643
8741
  this.keyManager.setBufferFromKey(rid, this.ridBuffer);
8644
8742
  const pageId = this.keyManager.getPageId(this.ridBuffer);
@@ -8836,18 +8934,24 @@ var require_cjs = __commonJS({
8836
8934
  await hook();
8837
8935
  }
8838
8936
  });
8839
- if (this.pfs.wal && this.dirtyPages.size > 0) {
8840
- await this.pfs.wal.prepareCommit(this.dirtyPages);
8841
- await this.pfs.wal.writeCommitMarker();
8842
- }
8843
- for (const [pageId, data] of this.dirtyPages) {
8844
- await this.pageStrategy.write(pageId, data);
8845
- }
8846
- if (this.pfs.wal) {
8847
- this.pfs.wal.incrementWrittenPages(this.dirtyPages.size);
8848
- if (this.pfs.wal.shouldCheckpoint(this.pfs.options.walCheckpointThreshold)) {
8849
- await this.pfs.wal.clear();
8937
+ let shouldTriggerCheckpoint = false;
8938
+ await this.pfs.runGlobalLock(async () => {
8939
+ if (this.pfs.wal && this.dirtyPages.size > 0) {
8940
+ await this.pfs.wal.prepareCommit(this.dirtyPages);
8941
+ await this.pfs.wal.writeCommitMarker();
8850
8942
  }
8943
+ for (const [pageId, data] of this.dirtyPages) {
8944
+ await this.pageStrategy.write(pageId, data);
8945
+ }
8946
+ if (this.pfs.wal) {
8947
+ this.pfs.wal.incrementWrittenPages(this.dirtyPages.size);
8948
+ if (this.pfs.wal.shouldCheckpoint(this.pfs.options.walCheckpointThreshold)) {
8949
+ shouldTriggerCheckpoint = true;
8950
+ }
8951
+ }
8952
+ });
8953
+ if (shouldTriggerCheckpoint) {
8954
+ await this.pfs.checkpoint();
8851
8955
  }
8852
8956
  this.dirtyPages.clear();
8853
8957
  this.undoPages.clear();
@@ -9258,6 +9362,19 @@ var require_cjs = __commonJS({
9258
9362
  return this.textCodec.decode(data);
9259
9363
  }, tx);
9260
9364
  }
9365
+ async selectMany(pks, asRaw = false, tx) {
9366
+ if (!this.initialized) {
9367
+ throw new Error("Dataply instance is not initialized");
9368
+ }
9369
+ return this.runWithDefault(async (tx2) => {
9370
+ const results = await this.rowTableEngine.selectMany(pks, tx2);
9371
+ return results.map((data) => {
9372
+ if (data === null) return null;
9373
+ if (asRaw) return data;
9374
+ return this.textCodec.decode(data);
9375
+ });
9376
+ }, tx);
9377
+ }
9261
9378
  /**
9262
9379
  * Closes the dataply file.
9263
9380
  */
@@ -9346,6 +9463,9 @@ var require_cjs = __commonJS({
9346
9463
  async select(pk, asRaw = false, tx) {
9347
9464
  return this.api.select(pk, asRaw, tx);
9348
9465
  }
9466
+ async selectMany(pks, asRaw = false, tx) {
9467
+ return this.api.selectMany(pks, asRaw, tx);
9468
+ }
9349
9469
  /**
9350
9470
  * Closes the dataply file.
9351
9471
  */
@@ -9406,6 +9526,7 @@ __export(src_exports, {
9406
9526
  module.exports = __toCommonJS(src_exports);
9407
9527
 
9408
9528
  // src/core/document.ts
9529
+ var os = __toESM(require("node:os"));
9409
9530
  var import_dataply3 = __toESM(require_cjs());
9410
9531
 
9411
9532
  // src/core/bptree/documentStrategy.ts
@@ -10118,7 +10239,9 @@ var DocumentDataply = class _DocumentDataply {
10118
10239
  } = options;
10119
10240
  const self = this;
10120
10241
  const stream = this.api.streamWithDefault(async function* (tx2) {
10121
- const keys = await self.getKeys(query, orderByField, sortOrder);
10242
+ const keySet = await self.getKeys(query, orderByField, sortOrder);
10243
+ const keys = new Uint32Array(keySet);
10244
+ const totalKeys = keys.length;
10122
10245
  const selectivity = await self.getSelectivityCandidate(
10123
10246
  self.verboseQuery(query),
10124
10247
  orderByField
@@ -10127,12 +10250,29 @@ var DocumentDataply = class _DocumentDataply {
10127
10250
  if (selectivity) {
10128
10251
  selectivity.rollback();
10129
10252
  }
10253
+ let CHUNK_SIZE = 100;
10130
10254
  if (!isDriverOrderByField && orderByField) {
10131
10255
  const results = [];
10132
- for (const key of keys) {
10133
- const stringified = await self.api.select(key, false, tx2);
10134
- if (!stringified) continue;
10135
- results.push(JSON.parse(stringified));
10256
+ let i = 0;
10257
+ while (i < totalKeys) {
10258
+ const chunk = Array.from(keys.subarray(i, i + CHUNK_SIZE));
10259
+ const stringifiedResults = await self.api.selectMany(chunk, false, tx2);
10260
+ if (i === 0) {
10261
+ let totalBytes = 0;
10262
+ let count = 0;
10263
+ for (const s of stringifiedResults) {
10264
+ if (s) {
10265
+ totalBytes += s.length;
10266
+ count++;
10267
+ }
10268
+ }
10269
+ const avgSize = count > 0 ? totalBytes / count : 1024;
10270
+ CHUNK_SIZE = Math.max(32, Math.floor(os.freemem() * 0.1 / avgSize));
10271
+ }
10272
+ for (const stringified of stringifiedResults) {
10273
+ if (stringified) results.push(JSON.parse(stringified));
10274
+ }
10275
+ i += chunk.length;
10136
10276
  }
10137
10277
  results.sort((a, b) => {
10138
10278
  const aVal = a[orderByField] ?? a._id;
@@ -10147,19 +10287,36 @@ var DocumentDataply = class _DocumentDataply {
10147
10287
  yield doc;
10148
10288
  }
10149
10289
  } else {
10150
- let i = 0;
10151
10290
  let yieldedCount = 0;
10152
- for (const key of keys) {
10153
- if (yieldedCount >= limit) break;
10154
- if (i < offset) {
10155
- i++;
10156
- continue;
10291
+ let i = offset;
10292
+ let isFirst = true;
10293
+ while (i < totalKeys && yieldedCount < limit) {
10294
+ const currentChunkLimit = isFirst ? 100 : CHUNK_SIZE;
10295
+ const remainingLimit = limit - yieldedCount;
10296
+ const pksToFetchCount = Math.min(currentChunkLimit, remainingLimit);
10297
+ const chunk = Array.from(keys.subarray(i, i + pksToFetchCount));
10298
+ const stringifiedResults = await self.api.selectMany(chunk, false, tx2);
10299
+ if (isFirst) {
10300
+ let totalBytes = 0;
10301
+ let count = 0;
10302
+ for (const s of stringifiedResults) {
10303
+ if (s) {
10304
+ totalBytes += s.length;
10305
+ count++;
10306
+ }
10307
+ }
10308
+ const avgSize = count > 0 ? totalBytes / count : 1024;
10309
+ CHUNK_SIZE = Math.max(32, Math.floor(os.freemem() * 0.1 / avgSize));
10310
+ isFirst = false;
10311
+ }
10312
+ for (const stringified of stringifiedResults) {
10313
+ if (stringified) {
10314
+ yield JSON.parse(stringified);
10315
+ yieldedCount++;
10316
+ if (yieldedCount >= limit) break;
10317
+ }
10157
10318
  }
10158
- const stringified = await self.api.select(key, false, tx2);
10159
- if (!stringified) continue;
10160
- yield JSON.parse(stringified);
10161
- yieldedCount++;
10162
- i++;
10319
+ i += chunk.length;
10163
10320
  }
10164
10321
  }
10165
10322
  }, tx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-dataply",
3
- "version": "0.0.4-alpha.3",
3
+ "version": "0.0.4-alpha.5",
4
4
  "description": "Simple and powerful JSON document database supporting complex queries and flexible indexing policies.",
5
5
  "license": "MIT",
6
6
  "author": "izure <admin@izure.org>",
@@ -42,7 +42,7 @@
42
42
  "dataply"
43
43
  ],
44
44
  "dependencies": {
45
- "dataply": "^0.0.20-alpha.2"
45
+ "dataply": "^0.0.20-alpha.6"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/jest": "^30.0.0",
@@ -51,4 +51,4 @@
51
51
  "ts-jest": "^29.4.6",
52
52
  "typescript": "^5.9.3"
53
53
  }
54
- }
54
+ }