document-dataply 0.0.6 → 0.0.7-alpha.1

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
@@ -1757,6 +1757,39 @@ var require_cjs = __commonJS({
1757
1757
  }
1758
1758
  return true;
1759
1759
  }
1760
+ /**
1761
+ * Inserts a key-value pair into an already-cloned leaf node in-place.
1762
+ * Unlike _insertAtLeaf, this does NOT clone or update the node via MVCC.
1763
+ * Used by batchInsert to batch multiple insertions with a single clone/update.
1764
+ * @returns true if the leaf was modified, false if the key already exists.
1765
+ */
1766
+ _insertValueIntoLeaf(leaf, key, value) {
1767
+ if (leaf.values.length) {
1768
+ for (let i = 0, len = leaf.values.length; i < len; i++) {
1769
+ const nValue = leaf.values[i];
1770
+ if (this.comparator.isSame(value, nValue)) {
1771
+ if (leaf.keys[i].includes(key)) {
1772
+ return false;
1773
+ }
1774
+ leaf.keys[i].push(key);
1775
+ return true;
1776
+ } else if (this.comparator.isLower(value, nValue)) {
1777
+ leaf.values.splice(i, 0, value);
1778
+ leaf.keys.splice(i, 0, [key]);
1779
+ return true;
1780
+ } else if (i + 1 === leaf.values.length) {
1781
+ leaf.values.push(value);
1782
+ leaf.keys.push([key]);
1783
+ return true;
1784
+ }
1785
+ }
1786
+ } else {
1787
+ leaf.values = [value];
1788
+ leaf.keys = [[key]];
1789
+ return true;
1790
+ }
1791
+ return false;
1792
+ }
1760
1793
  _cloneNode(node) {
1761
1794
  return JSON.parse(JSON.stringify(node));
1762
1795
  }
@@ -2213,17 +2246,24 @@ var require_cjs = __commonJS({
2213
2246
  }
2214
2247
  return void 0;
2215
2248
  }
2216
- *keysStream(condition, filterValues, limit, order = "asc") {
2217
- const stream = this.whereStream(condition, limit, order);
2249
+ *keysStream(condition, options) {
2250
+ const { filterValues, limit, order = "asc" } = options ?? {};
2251
+ const stream = this.whereStream(condition, options);
2218
2252
  const intersection = filterValues && filterValues.size > 0 ? filterValues : null;
2253
+ let count = 0;
2219
2254
  for (const [key] of stream) {
2220
2255
  if (intersection && !intersection.has(key)) {
2221
2256
  continue;
2222
2257
  }
2223
2258
  yield key;
2259
+ count++;
2260
+ if (limit !== void 0 && count >= limit) {
2261
+ break;
2262
+ }
2224
2263
  }
2225
2264
  }
2226
- *whereStream(condition, limit, order = "asc") {
2265
+ *whereStream(condition, options) {
2266
+ const { filterValues, limit, order = "asc" } = options ?? {};
2227
2267
  const driverKey = this.getDriverKey(condition);
2228
2268
  if (!driverKey) return;
2229
2269
  const value = condition[driverKey];
@@ -2246,8 +2286,12 @@ var require_cjs = __commonJS({
2246
2286
  earlyTerminate
2247
2287
  );
2248
2288
  let count = 0;
2289
+ const intersection = filterValues && filterValues.size > 0 ? filterValues : null;
2249
2290
  for (const pair of generator) {
2250
2291
  const [k, v] = pair;
2292
+ if (intersection && !intersection.has(k)) {
2293
+ continue;
2294
+ }
2251
2295
  let isMatch = true;
2252
2296
  for (const key in condition) {
2253
2297
  if (key === driverKey) continue;
@@ -2267,16 +2311,16 @@ var require_cjs = __commonJS({
2267
2311
  }
2268
2312
  }
2269
2313
  }
2270
- keys(condition, filterValues, order = "asc") {
2314
+ keys(condition, options) {
2271
2315
  const set = /* @__PURE__ */ new Set();
2272
- for (const key of this.keysStream(condition, filterValues, void 0, order)) {
2316
+ for (const key of this.keysStream(condition, options)) {
2273
2317
  set.add(key);
2274
2318
  }
2275
2319
  return set;
2276
2320
  }
2277
- where(condition, order = "asc") {
2321
+ where(condition, options) {
2278
2322
  const map = /* @__PURE__ */ new Map();
2279
- for (const [key, value] of this.whereStream(condition, void 0, order)) {
2323
+ for (const [key, value] of this.whereStream(condition, options)) {
2280
2324
  map.set(key, value);
2281
2325
  }
2282
2326
  return map;
@@ -2304,6 +2348,50 @@ var require_cjs = __commonJS({
2304
2348
  this._insertInParent(before, after.values[0], after);
2305
2349
  }
2306
2350
  }
2351
+ batchInsert(entries) {
2352
+ if (entries.length === 0) return;
2353
+ const sorted = [...entries].sort((a, b) => this.comparator.asc(a[1], b[1]));
2354
+ let currentLeaf = null;
2355
+ let modified = false;
2356
+ for (const [key, value] of sorted) {
2357
+ const targetLeaf = this.insertableNode(value);
2358
+ if (currentLeaf !== null && currentLeaf.id === targetLeaf.id) {
2359
+ } else {
2360
+ if (currentLeaf !== null && modified) {
2361
+ this._updateNode(currentLeaf);
2362
+ }
2363
+ currentLeaf = this._cloneNode(targetLeaf);
2364
+ modified = false;
2365
+ }
2366
+ const changed = this._insertValueIntoLeaf(currentLeaf, key, value);
2367
+ modified = modified || changed;
2368
+ if (currentLeaf.values.length === this.order) {
2369
+ this._updateNode(currentLeaf);
2370
+ let after = this._createNode(
2371
+ true,
2372
+ [],
2373
+ [],
2374
+ currentLeaf.parent,
2375
+ null,
2376
+ null
2377
+ );
2378
+ const mid = Math.ceil(this.order / 2) - 1;
2379
+ after = this._cloneNode(after);
2380
+ after.values = currentLeaf.values.slice(mid + 1);
2381
+ after.keys = currentLeaf.keys.slice(mid + 1);
2382
+ currentLeaf.values = currentLeaf.values.slice(0, mid + 1);
2383
+ currentLeaf.keys = currentLeaf.keys.slice(0, mid + 1);
2384
+ this._updateNode(currentLeaf);
2385
+ this._updateNode(after);
2386
+ this._insertInParent(currentLeaf, after.values[0], after);
2387
+ currentLeaf = null;
2388
+ modified = false;
2389
+ }
2390
+ }
2391
+ if (currentLeaf !== null && modified) {
2392
+ this._updateNode(currentLeaf);
2393
+ }
2394
+ }
2307
2395
  _deleteEntry(node, key) {
2308
2396
  if (!node.leaf) {
2309
2397
  let keyIndex = -1;
@@ -2655,6 +2743,14 @@ var require_cjs = __commonJS({
2655
2743
  throw new Error(`Transaction failed: ${result.error || "Commit failed due to conflict"}`);
2656
2744
  }
2657
2745
  }
2746
+ batchInsert(entries) {
2747
+ const tx = this.createTransaction();
2748
+ tx.batchInsert(entries);
2749
+ const result = tx.commit();
2750
+ if (!result.success) {
2751
+ throw new Error(`Transaction failed: ${result.error || "Commit failed due to conflict"}`);
2752
+ }
2753
+ }
2658
2754
  };
2659
2755
  var Ryoiki22 = class _Ryoiki2 {
2660
2756
  readings;
@@ -3280,17 +3376,24 @@ var require_cjs = __commonJS({
3280
3376
  }
3281
3377
  return void 0;
3282
3378
  }
3283
- async *keysStream(condition, filterValues, limit, order = "asc") {
3284
- const stream = this.whereStream(condition, limit, order);
3379
+ async *keysStream(condition, options) {
3380
+ const { filterValues, limit, order = "asc" } = options ?? {};
3381
+ const stream = this.whereStream(condition, options);
3285
3382
  const intersection = filterValues && filterValues.size > 0 ? filterValues : null;
3383
+ let count = 0;
3286
3384
  for await (const [key] of stream) {
3287
3385
  if (intersection && !intersection.has(key)) {
3288
3386
  continue;
3289
3387
  }
3290
3388
  yield key;
3389
+ count++;
3390
+ if (limit !== void 0 && count >= limit) {
3391
+ break;
3392
+ }
3291
3393
  }
3292
3394
  }
3293
- async *whereStream(condition, limit, order = "asc") {
3395
+ async *whereStream(condition, options) {
3396
+ const { filterValues, limit, order = "asc" } = options ?? {};
3294
3397
  const driverKey = this.getDriverKey(condition);
3295
3398
  if (!driverKey) return;
3296
3399
  const value = condition[driverKey];
@@ -3313,8 +3416,12 @@ var require_cjs = __commonJS({
3313
3416
  earlyTerminate
3314
3417
  );
3315
3418
  let count = 0;
3419
+ const intersection = filterValues && filterValues.size > 0 ? filterValues : null;
3316
3420
  for await (const pair of generator) {
3317
3421
  const [k, v] = pair;
3422
+ if (intersection && !intersection.has(k)) {
3423
+ continue;
3424
+ }
3318
3425
  let isMatch = true;
3319
3426
  for (const key in condition) {
3320
3427
  if (key === driverKey) continue;
@@ -3334,16 +3441,16 @@ var require_cjs = __commonJS({
3334
3441
  }
3335
3442
  }
3336
3443
  }
3337
- async keys(condition, filterValues, order = "asc") {
3444
+ async keys(condition, options) {
3338
3445
  const set = /* @__PURE__ */ new Set();
3339
- for await (const key of this.keysStream(condition, filterValues, void 0, order)) {
3446
+ for await (const key of this.keysStream(condition, options)) {
3340
3447
  set.add(key);
3341
3448
  }
3342
3449
  return set;
3343
3450
  }
3344
- async where(condition, order = "asc") {
3451
+ async where(condition, options) {
3345
3452
  const map = /* @__PURE__ */ new Map();
3346
- for await (const [key, value] of this.whereStream(condition, void 0, order)) {
3453
+ for await (const [key, value] of this.whereStream(condition, options)) {
3347
3454
  map.set(key, value);
3348
3455
  }
3349
3456
  return map;
@@ -3373,6 +3480,52 @@ var require_cjs = __commonJS({
3373
3480
  }
3374
3481
  });
3375
3482
  }
3483
+ async batchInsert(entries) {
3484
+ if (entries.length === 0) return;
3485
+ return this.writeLock(0, async () => {
3486
+ const sorted = [...entries].sort((a, b) => this.comparator.asc(a[1], b[1]));
3487
+ let currentLeaf = null;
3488
+ let modified = false;
3489
+ for (const [key, value] of sorted) {
3490
+ const targetLeaf = await this.insertableNode(value);
3491
+ if (currentLeaf !== null && currentLeaf.id === targetLeaf.id) {
3492
+ } else {
3493
+ if (currentLeaf !== null && modified) {
3494
+ await this._updateNode(currentLeaf);
3495
+ }
3496
+ currentLeaf = this._cloneNode(targetLeaf);
3497
+ modified = false;
3498
+ }
3499
+ const changed = this._insertValueIntoLeaf(currentLeaf, key, value);
3500
+ modified = modified || changed;
3501
+ if (currentLeaf.values.length === this.order) {
3502
+ await this._updateNode(currentLeaf);
3503
+ let after = await this._createNode(
3504
+ true,
3505
+ [],
3506
+ [],
3507
+ currentLeaf.parent,
3508
+ null,
3509
+ null
3510
+ );
3511
+ const mid = Math.ceil(this.order / 2) - 1;
3512
+ after = this._cloneNode(after);
3513
+ after.values = currentLeaf.values.slice(mid + 1);
3514
+ after.keys = currentLeaf.keys.slice(mid + 1);
3515
+ currentLeaf.values = currentLeaf.values.slice(0, mid + 1);
3516
+ currentLeaf.keys = currentLeaf.keys.slice(0, mid + 1);
3517
+ await this._updateNode(currentLeaf);
3518
+ await this._updateNode(after);
3519
+ await this._insertInParent(currentLeaf, after.values[0], after);
3520
+ currentLeaf = null;
3521
+ modified = false;
3522
+ }
3523
+ }
3524
+ if (currentLeaf !== null && modified) {
3525
+ await this._updateNode(currentLeaf);
3526
+ }
3527
+ });
3528
+ }
3376
3529
  async _deleteEntry(node, key) {
3377
3530
  if (!node.leaf) {
3378
3531
  let keyIndex = -1;
@@ -3730,6 +3883,16 @@ var require_cjs = __commonJS({
3730
3883
  }
3731
3884
  });
3732
3885
  }
3886
+ async batchInsert(entries) {
3887
+ return this.writeLock(1, async () => {
3888
+ const tx = await this.createTransaction();
3889
+ await tx.batchInsert(entries);
3890
+ const result = await tx.commit();
3891
+ if (!result.success) {
3892
+ throw new Error(`Transaction failed: ${result.error || "Commit failed due to conflict"}`);
3893
+ }
3894
+ });
3895
+ }
3733
3896
  };
3734
3897
  var SerializeStrategy = class {
3735
3898
  order;
@@ -8704,6 +8867,7 @@ var require_cjs = __commonJS({
8704
8867
  if (!this.factory.isDataPage(lastInsertDataPage)) {
8705
8868
  throw new Error(`Last insert page is not data page`);
8706
8869
  }
8870
+ const batchInsertData = [];
8707
8871
  for (const data of dataList) {
8708
8872
  const pk = ++lastPk;
8709
8873
  const willRowSize = this.getRequiredRowSize(data);
@@ -8748,9 +8912,10 @@ var require_cjs = __commonJS({
8748
8912
  await this.pfs.setPage(lastInsertDataPageId, lastInsertDataPage, tx);
8749
8913
  }
8750
8914
  }
8751
- await btx.insert(this.getRID(), pk);
8915
+ batchInsertData.push([this.getRID(), pk]);
8752
8916
  pks.push(pk);
8753
8917
  }
8918
+ await btx.batchInsert(batchInsertData);
8754
8919
  tx.__markBPTreeDirty();
8755
8920
  const freshMetadataPage = await this.pfs.getMetadata(tx);
8756
8921
  this.metadataPageManager.setLastInsertPageId(freshMetadataPage, lastInsertDataPageId);
@@ -9953,6 +10118,45 @@ var BinaryHeap = class {
9953
10118
  }
9954
10119
  };
9955
10120
 
10121
+ // src/utils/tokenizer.ts
10122
+ function whitespaceTokenize(text) {
10123
+ if (typeof text !== "string") return [];
10124
+ return Array.from(new Set(text.split(/\s+/).filter(Boolean)));
10125
+ }
10126
+ function ngramTokenize(text, gramSize) {
10127
+ if (typeof text !== "string") return [];
10128
+ const tokens = /* @__PURE__ */ new Set();
10129
+ const words = text.split(/\s+/).filter(Boolean);
10130
+ for (const word of words) {
10131
+ if (word.length < gramSize) {
10132
+ if (word.length > 0) tokens.add(word);
10133
+ continue;
10134
+ }
10135
+ for (let i = 0; i <= word.length - gramSize; i++) {
10136
+ tokens.add(word.slice(i, i + gramSize));
10137
+ }
10138
+ }
10139
+ return Array.from(tokens);
10140
+ }
10141
+ function tokenize(text, options) {
10142
+ if (options.tokenizer === "whitespace") {
10143
+ return whitespaceTokenize(text);
10144
+ }
10145
+ if (options.tokenizer === "ngram") {
10146
+ return ngramTokenize(text, options.gramSize);
10147
+ }
10148
+ return [];
10149
+ }
10150
+
10151
+ // src/utils/hash.ts
10152
+ function fastStringHash(str) {
10153
+ let hash = 0;
10154
+ for (let i = 0; i < str.length; i++) {
10155
+ hash = (hash << 5) - hash + str.charCodeAt(i) | 0;
10156
+ }
10157
+ return hash >>> 0;
10158
+ }
10159
+
9956
10160
  // src/core/documentAPI.ts
9957
10161
  var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
9958
10162
  indices = {};
@@ -10008,7 +10212,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10008
10212
  } else {
10009
10213
  const [_pk, isMetaBackfillEnabled] = existingIndex;
10010
10214
  if (isBackfillEnabled && !isMetaBackfillEnabled) {
10011
- metadata.indices[field][1] = true;
10215
+ metadata.indices[field][1] = isBackfillEnabled;
10012
10216
  isMetadataChanged = true;
10013
10217
  backfillTargets.push(field);
10014
10218
  } else if (!isBackfillEnabled && isMetaBackfillEnabled) {
@@ -10111,12 +10315,23 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10111
10315
  }
10112
10316
  const v = flatDoc[field];
10113
10317
  const btx = fieldTxMap[field];
10114
- const entry = { k, v };
10115
- await btx.insert(k, entry);
10116
- if (!fieldMap.has(btx)) {
10117
- fieldMap.set(btx, []);
10318
+ const indexConfig = metadata.indices[field]?.[1];
10319
+ const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts" && typeof v === "string";
10320
+ let tokens = [v];
10321
+ if (isFts) {
10322
+ tokens = tokenize(v, indexConfig);
10323
+ }
10324
+ const batchInsertData = [];
10325
+ for (const token of tokens) {
10326
+ const keyToInsert = isFts ? this.getTokenKey(k, token) : k;
10327
+ const entry = { k, v: token };
10328
+ batchInsertData.push([keyToInsert, entry]);
10329
+ if (!fieldMap.has(btx)) {
10330
+ fieldMap.set(btx, []);
10331
+ }
10332
+ fieldMap.get(btx).push({ k: keyToInsert, v: entry });
10118
10333
  }
10119
- fieldMap.get(btx).push(entry);
10334
+ await btx.batchInsert(batchInsertData);
10120
10335
  }
10121
10336
  backfilledCount++;
10122
10337
  }
@@ -10210,6 +10425,11 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10210
10425
  async updateDocumentInnerMetadata(metadata, tx) {
10211
10426
  await this.update(1, JSON.stringify(metadata), tx);
10212
10427
  }
10428
+ /**
10429
+ * Transforms a query object into a verbose query object
10430
+ * @param query The query object to transform
10431
+ * @returns The verbose query object
10432
+ */
10213
10433
  verboseQuery(query) {
10214
10434
  const result = {};
10215
10435
  for (const field in query) {
@@ -10223,7 +10443,12 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10223
10443
  const before = operator;
10224
10444
  const after = this.operatorConverters[before];
10225
10445
  const v = conditions[before];
10226
- if (!after) continue;
10446
+ if (!after) {
10447
+ if (before === "match") {
10448
+ newConditions[before] = v;
10449
+ }
10450
+ continue;
10451
+ }
10227
10452
  if (before === "or" && Array.isArray(v)) {
10228
10453
  newConditions[after] = v.map((val) => ({ v: val }));
10229
10454
  } else if (before === "like") {
@@ -10245,12 +10470,26 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10245
10470
  */
10246
10471
  async getSelectivityCandidate(query, orderByField) {
10247
10472
  const candidates = [];
10473
+ const metadata = await this.getDocumentInnerMetadata(this.txContext.get());
10248
10474
  for (const field in query) {
10249
10475
  const tree = this.trees.get(field);
10250
10476
  if (!tree) continue;
10251
10477
  const condition = query[field];
10252
10478
  const treeTx = await tree.createTransaction();
10253
- candidates.push({ tree: treeTx, condition, field });
10479
+ const indexConfig = metadata.indices[field]?.[1];
10480
+ let isFtsMatch = false;
10481
+ let matchTokens;
10482
+ if (typeof indexConfig === "object" && indexConfig?.type === "fts" && condition.match) {
10483
+ isFtsMatch = true;
10484
+ matchTokens = tokenize(condition.match, indexConfig);
10485
+ }
10486
+ candidates.push({
10487
+ tree: treeTx,
10488
+ condition,
10489
+ field,
10490
+ isFtsMatch,
10491
+ matchTokens
10492
+ });
10254
10493
  }
10255
10494
  const rollback = () => {
10256
10495
  for (const { tree } of candidates) {
@@ -10293,36 +10532,69 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10293
10532
  const smallChunkSize = safeLimit * 0.3;
10294
10533
  return { verySmallChunkSize, smallChunkSize };
10295
10534
  }
10535
+ getTokenKey(pk, token) {
10536
+ return fastStringHash(pk + ":" + token);
10537
+ }
10538
+ async applyCandidateByFTS(candidate, matchedTokens, filterValues, order) {
10539
+ const keys = /* @__PURE__ */ new Set();
10540
+ for (const token of matchedTokens) {
10541
+ const pairs = await candidate.tree.where(
10542
+ { primaryEqual: { v: token } },
10543
+ { order }
10544
+ );
10545
+ for (const c of pairs.values()) {
10546
+ if (!c || typeof c.k !== "number") continue;
10547
+ const dpk = c.k;
10548
+ if (filterValues === void 0 || filterValues.has(dpk)) {
10549
+ keys.add(dpk);
10550
+ }
10551
+ }
10552
+ }
10553
+ return keys;
10554
+ }
10555
+ /**
10556
+ * 특정 인덱스 후보를 조회하여 PK 집합을 필터링합니다.
10557
+ */
10558
+ async applyCandidate(candidate, filterValues, order) {
10559
+ return await candidate.tree.keys(
10560
+ candidate.condition,
10561
+ {
10562
+ filterValues,
10563
+ order
10564
+ }
10565
+ );
10566
+ }
10296
10567
  /**
10297
- * Get Primary Keys based on query and index selection.
10298
- * Internal common method to unify query optimization.
10568
+ * 쿼리와 인덱스 선택을 기반으로 기본 키(Primary Keys)를 가져옵니다.
10569
+ * 쿼리 최적화를 통합하기 위한 내부 공통 메서드입니다.
10299
10570
  */
10300
10571
  async getKeys(query, orderBy, sortOrder = "asc") {
10301
10572
  const isQueryEmpty = Object.keys(query).length === 0;
10302
10573
  const normalizedQuery = isQueryEmpty ? { _id: { gte: 0 } } : query;
10303
- const verbose = this.verboseQuery(normalizedQuery);
10304
10574
  const selectivity = await this.getSelectivityCandidate(
10305
- verbose,
10575
+ this.verboseQuery(normalizedQuery),
10306
10576
  orderBy
10307
10577
  );
10308
10578
  if (!selectivity) return new Float64Array(0);
10309
10579
  const { driver, others, rollback } = selectivity;
10310
- const isDriverOrderByField = orderBy === void 0 || driver.field === orderBy;
10311
- if (isDriverOrderByField) {
10312
- let keysSet = await driver.tree.keys(driver.condition, void 0, sortOrder);
10313
- for (const { tree, condition } of others) {
10314
- keysSet = await tree.keys(condition, keysSet, sortOrder);
10315
- }
10316
- rollback();
10317
- return new Float64Array(keysSet);
10318
- } else {
10319
- let keysSet = await driver.tree.keys(driver.condition, void 0);
10320
- for (const { tree, condition } of others) {
10321
- keysSet = await tree.keys(condition, keysSet);
10580
+ const useIndexOrder = orderBy === void 0 || driver.field === orderBy;
10581
+ const candidates = [driver, ...others];
10582
+ let keys = void 0;
10583
+ for (const candidate of candidates) {
10584
+ const currentOrder = useIndexOrder ? sortOrder : void 0;
10585
+ if (candidate.isFtsMatch && candidate.matchTokens && candidate.matchTokens.length > 0) {
10586
+ keys = await this.applyCandidateByFTS(
10587
+ candidate,
10588
+ candidate.matchTokens,
10589
+ keys,
10590
+ currentOrder
10591
+ );
10592
+ } else {
10593
+ keys = await this.applyCandidate(candidate, keys, currentOrder);
10322
10594
  }
10323
- rollback();
10324
- return new Float64Array(keysSet);
10325
10595
  }
10596
+ rollback();
10597
+ return new Float64Array(Array.from(keys || []));
10326
10598
  }
10327
10599
  async insertDocumentInternal(document, tx) {
10328
10600
  const metadata = await this.getDocumentInnerMetadata(tx);
@@ -10346,15 +10618,25 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10346
10618
  */
10347
10619
  async insertSingleDocument(document, tx) {
10348
10620
  return this.writeLock(() => this.runWithDefault(async (tx2) => {
10349
- const { pk, document: dataplyDocument } = await this.insertDocumentInternal(document, tx2);
10621
+ const { pk: dpk, document: dataplyDocument } = await this.insertDocumentInternal(document, tx2);
10622
+ const metadata = await this.getDocumentInnerMetadata(tx2);
10350
10623
  const flattenDocument = this.flattenDocument(dataplyDocument);
10351
10624
  for (const field in flattenDocument) {
10352
10625
  const tree = this.trees.get(field);
10353
10626
  if (!tree) continue;
10354
10627
  const v = flattenDocument[field];
10355
- const [error] = await catchPromise(tree.insert(pk, { k: pk, v }));
10356
- if (error) {
10357
- console.error(`BPTree indexing failed for field: ${field}`, error);
10628
+ const indexConfig = metadata.indices[field]?.[1];
10629
+ let tokens = [v];
10630
+ const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts" && typeof v === "string";
10631
+ if (isFts) {
10632
+ tokens = tokenize(v, indexConfig);
10633
+ }
10634
+ for (const token of tokens) {
10635
+ const keyToInsert = isFts ? this.getTokenKey(dpk, token) : dpk;
10636
+ const [error] = await catchPromise(tree.insert(keyToInsert, { k: dpk, v: token }));
10637
+ if (error) {
10638
+ throw error;
10639
+ }
10358
10640
  }
10359
10641
  }
10360
10642
  return dataplyDocument._id;
@@ -10392,14 +10674,25 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10392
10674
  }
10393
10675
  for (const [field, tree] of this.trees) {
10394
10676
  const treeTx = await tree.createTransaction();
10677
+ const indexConfig = metadata.indices[field]?.[1];
10678
+ const batchInsertData = [];
10395
10679
  for (let i = 0, len = flattenedData.length; i < len; i++) {
10396
10680
  const item = flattenedData[i];
10397
10681
  const v = item.data[field];
10398
10682
  if (v === void 0) continue;
10399
- const [error] = await catchPromise(treeTx.insert(item.pk, { k: item.pk, v }));
10400
- if (error) {
10401
- console.error(`BPTree indexing failed for field: ${field}`, error);
10683
+ const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts" && typeof v === "string";
10684
+ let tokens = [v];
10685
+ if (isFts) {
10686
+ tokens = tokenize(v, indexConfig);
10402
10687
  }
10688
+ for (const token of tokens) {
10689
+ const keyToInsert = isFts ? this.getTokenKey(item.pk, token) : item.pk;
10690
+ batchInsertData.push([keyToInsert, { k: item.pk, v: token }]);
10691
+ }
10692
+ }
10693
+ const [error] = await catchPromise(treeTx.batchInsert(batchInsertData));
10694
+ if (error) {
10695
+ throw error;
10403
10696
  }
10404
10697
  const res = await treeTx.commit();
10405
10698
  if (!res.success) {
@@ -10431,15 +10724,34 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10431
10724
  const updatedDoc = computeUpdatedDoc(doc);
10432
10725
  const oldFlatDoc = this.flattenDocument(doc);
10433
10726
  const newFlatDoc = this.flattenDocument(updatedDoc);
10727
+ const metadata = await this.getDocumentInnerMetadata(tx);
10434
10728
  for (const [field, treeTx] of treeTxs) {
10435
10729
  const oldV = oldFlatDoc[field];
10436
10730
  const newV = newFlatDoc[field];
10437
10731
  if (oldV === newV) continue;
10732
+ const indexConfig = metadata.indices[field]?.[1];
10733
+ const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts";
10438
10734
  if (field in oldFlatDoc) {
10439
- await treeTx.delete(pk, { k: pk, v: oldV });
10735
+ let oldTokens = [oldV];
10736
+ if (isFts && typeof oldV === "string") {
10737
+ oldTokens = tokenize(oldV, indexConfig);
10738
+ }
10739
+ for (const oldToken of oldTokens) {
10740
+ const keyToDelete = isFts ? this.getTokenKey(pk, oldToken) : pk;
10741
+ await treeTx.delete(keyToDelete, { k: pk, v: oldToken });
10742
+ }
10440
10743
  }
10441
10744
  if (field in newFlatDoc) {
10442
- await treeTx.insert(pk, { k: pk, v: newV });
10745
+ let newTokens = [newV];
10746
+ if (isFts && typeof newV === "string") {
10747
+ newTokens = tokenize(newV, indexConfig);
10748
+ }
10749
+ const batchInsertData = [];
10750
+ for (const newToken of newTokens) {
10751
+ const keyToInsert = isFts ? this.getTokenKey(pk, newToken) : pk;
10752
+ batchInsertData.push([keyToInsert, { k: pk, v: newToken }]);
10753
+ }
10754
+ await treeTx.batchInsert(batchInsertData);
10443
10755
  }
10444
10756
  }
10445
10757
  await this.update(pk, JSON.stringify(updatedDoc), tx);
@@ -10503,10 +10815,20 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10503
10815
  const doc = await this.getDocument(pk, tx2);
10504
10816
  if (!doc) continue;
10505
10817
  const flatDoc = this.flattenDocument(doc);
10818
+ const metadata = await this.getDocumentInnerMetadata(tx2);
10506
10819
  for (const [field, tree] of this.trees) {
10507
10820
  const v = flatDoc[field];
10508
10821
  if (v === void 0) continue;
10509
- await tree.delete(pk, { k: pk, v });
10822
+ const indexConfig = metadata.indices[field]?.[1];
10823
+ const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts" && typeof v === "string";
10824
+ let tokens = [v];
10825
+ if (isFts) {
10826
+ tokens = tokenize(v, indexConfig);
10827
+ }
10828
+ for (const token of tokens) {
10829
+ const keyToDelete = isFts ? this.getTokenKey(pk, token) : pk;
10830
+ await tree.delete(keyToDelete, { k: pk, v: token });
10831
+ }
10510
10832
  }
10511
10833
  await super.delete(pk, true, tx2);
10512
10834
  deletedCount++;
@@ -10526,6 +10848,63 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10526
10848
  return pks.length;
10527
10849
  }, tx));
10528
10850
  }
10851
+ /**
10852
+ * FTS 조건에 대해 문서가 유효한지 검증합니다.
10853
+ */
10854
+ verifyFts(doc, ftsConditions) {
10855
+ for (const { field, matchTokens } of ftsConditions) {
10856
+ const docValue = this.flattenDocument(doc)[field];
10857
+ if (typeof docValue !== "string") return false;
10858
+ for (const token of matchTokens) {
10859
+ if (!docValue.includes(token)) return false;
10860
+ }
10861
+ }
10862
+ return true;
10863
+ }
10864
+ /**
10865
+ * 메모리 기반으로 청크 크기를 동적 조절합니다.
10866
+ */
10867
+ adjustChunkSize(currentChunkSize, chunkTotalSize) {
10868
+ if (chunkTotalSize <= 0) return currentChunkSize;
10869
+ const { verySmallChunkSize, smallChunkSize } = this.getFreeMemoryChunkSize();
10870
+ if (chunkTotalSize < verySmallChunkSize) return currentChunkSize * 2;
10871
+ if (chunkTotalSize > smallChunkSize) return Math.max(Math.floor(currentChunkSize / 2), 20);
10872
+ return currentChunkSize;
10873
+ }
10874
+ /**
10875
+ * Prefetch 방식으로 키 배열을 청크 단위로 조회하여 문서를 순회합니다.
10876
+ * FTS 검증을 통과한 문서만 yield 합니다.
10877
+ */
10878
+ async *processChunkedKeys(keys, startIdx, initialChunkSize, ftsConditions, tx) {
10879
+ let i = startIdx;
10880
+ const totalKeys = keys.length;
10881
+ let currentChunkSize = initialChunkSize;
10882
+ let nextChunkPromise = null;
10883
+ if (i < totalKeys) {
10884
+ const endIdx = Math.min(i + currentChunkSize, totalKeys);
10885
+ nextChunkPromise = this.selectMany(keys.subarray(i, endIdx), false, tx);
10886
+ i = endIdx;
10887
+ }
10888
+ while (nextChunkPromise) {
10889
+ const rawResults = await nextChunkPromise;
10890
+ nextChunkPromise = null;
10891
+ if (i < totalKeys) {
10892
+ const endIdx = Math.min(i + currentChunkSize, totalKeys);
10893
+ nextChunkPromise = this.selectMany(keys.subarray(i, endIdx), false, tx);
10894
+ i = endIdx;
10895
+ }
10896
+ let chunkTotalSize = 0;
10897
+ for (let j = 0, len = rawResults.length; j < len; j++) {
10898
+ const s = rawResults[j];
10899
+ if (!s) continue;
10900
+ const doc = JSON.parse(s);
10901
+ chunkTotalSize += s.length * 2;
10902
+ if (!this.verifyFts(doc, ftsConditions)) continue;
10903
+ yield { doc, rawSize: s.length * 2 };
10904
+ }
10905
+ currentChunkSize = this.adjustChunkSize(currentChunkSize, chunkTotalSize);
10906
+ }
10907
+ }
10529
10908
  /**
10530
10909
  * Select documents from the database
10531
10910
  * @param query The query to use (only indexed fields + _id allowed)
@@ -10552,80 +10931,57 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10552
10931
  } = options;
10553
10932
  const self = this;
10554
10933
  const stream = this.streamWithDefault(async function* (tx2) {
10934
+ const metadata = await self.getDocumentInnerMetadata(tx2);
10935
+ const ftsConditions = [];
10936
+ for (const field in query) {
10937
+ const q = query[field];
10938
+ if (q && typeof q === "object" && "match" in q && typeof q.match === "string") {
10939
+ const indexConfig = metadata.indices[field]?.[1];
10940
+ if (typeof indexConfig === "object" && indexConfig?.type === "fts") {
10941
+ ftsConditions.push({ field, matchTokens: tokenize(q.match, indexConfig) });
10942
+ }
10943
+ }
10944
+ }
10555
10945
  const keys = await self.getKeys(query, orderByField, sortOrder);
10556
- const totalKeys = keys.length;
10557
- if (totalKeys === 0) return;
10946
+ if (keys.length === 0) return;
10558
10947
  const selectivity = await self.getSelectivityCandidate(
10559
10948
  self.verboseQuery(query),
10560
10949
  orderByField
10561
10950
  );
10562
10951
  const isDriverOrderByField = orderByField === void 0 || selectivity && selectivity.driver.field === orderByField;
10563
- if (selectivity) {
10564
- selectivity.rollback();
10565
- }
10566
- let currentChunkSize = self.options.pageSize;
10952
+ if (selectivity) selectivity.rollback();
10567
10953
  if (!isDriverOrderByField && orderByField) {
10568
10954
  const topK = limit === Infinity ? Infinity : offset + limit;
10569
10955
  let heap = null;
10570
10956
  if (topK !== Infinity) {
10571
- const heapComparator = (a, b) => {
10957
+ heap = new BinaryHeap((a, b) => {
10572
10958
  const aVal = a[orderByField] ?? a._id;
10573
10959
  const bVal = b[orderByField] ?? b._id;
10574
10960
  const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
10575
10961
  return sortOrder === "asc" ? -cmp : cmp;
10576
- };
10577
- heap = new BinaryHeap(heapComparator);
10962
+ });
10578
10963
  }
10579
10964
  const results = [];
10580
- let i = 0;
10581
- let nextChunkPromise = null;
10582
- if (i < totalKeys) {
10583
- const endIdx = Math.min(i + currentChunkSize, totalKeys);
10584
- const chunkKeys = keys.subarray(i, endIdx);
10585
- nextChunkPromise = self.selectMany(chunkKeys, false, tx2);
10586
- i = endIdx;
10587
- }
10588
- while (nextChunkPromise) {
10589
- const rawResults = await nextChunkPromise;
10590
- nextChunkPromise = null;
10591
- const { verySmallChunkSize, smallChunkSize } = self.getFreeMemoryChunkSize();
10592
- if (i < totalKeys) {
10593
- const endIdx = Math.min(i + currentChunkSize, totalKeys);
10594
- const chunkKeys = keys.subarray(i, endIdx);
10595
- nextChunkPromise = self.selectMany(chunkKeys, false, tx2);
10596
- i = endIdx;
10597
- }
10598
- let chunkTotalSize = 0;
10599
- for (let j = 0, len = rawResults.length; j < len; j++) {
10600
- const s = rawResults[j];
10601
- if (s) {
10602
- const doc = JSON.parse(s);
10603
- chunkTotalSize += s.length * 2;
10604
- if (heap) {
10605
- if (heap.size < topK) heap.push(doc);
10606
- else {
10607
- const top = heap.peek();
10608
- if (top) {
10609
- const aVal = doc[orderByField] ?? doc._id;
10610
- const bVal = top[orderByField] ?? top._id;
10611
- const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
10612
- const isBetter = sortOrder === "asc" ? cmp < 0 : cmp > 0;
10613
- if (isBetter) {
10614
- heap.replace(doc);
10615
- }
10616
- }
10617
- }
10618
- } else {
10619
- results.push(doc);
10965
+ for await (const { doc } of self.processChunkedKeys(
10966
+ keys,
10967
+ 0,
10968
+ self.options.pageSize,
10969
+ ftsConditions,
10970
+ tx2
10971
+ )) {
10972
+ if (heap) {
10973
+ if (heap.size < topK) heap.push(doc);
10974
+ else {
10975
+ const top = heap.peek();
10976
+ if (top) {
10977
+ const aVal = doc[orderByField] ?? doc._id;
10978
+ const bVal = top[orderByField] ?? top._id;
10979
+ const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
10980
+ if (sortOrder === "asc" ? cmp < 0 : cmp > 0) heap.replace(doc);
10620
10981
  }
10621
10982
  }
10622
- }
10623
- if (chunkTotalSize > 0) {
10624
- if (chunkTotalSize < verySmallChunkSize) {
10625
- currentChunkSize = currentChunkSize * 2;
10626
- } else if (chunkTotalSize > smallChunkSize) {
10627
- currentChunkSize = Math.max(Math.floor(currentChunkSize / 2), 20);
10628
- }
10983
+ } else {
10984
+ results.push(doc);
10629
10985
  }
10630
10986
  }
10631
10987
  const finalDocs = heap ? heap.toArray() : results;
@@ -10635,50 +10991,23 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10635
10991
  const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
10636
10992
  return sortOrder === "asc" ? cmp : -cmp;
10637
10993
  });
10638
- const start = offset;
10639
- const end = limit === Infinity ? void 0 : start + limit;
10640
- const limitedResults = finalDocs.slice(start, end);
10994
+ const end = limit === Infinity ? void 0 : offset + limit;
10995
+ const limitedResults = finalDocs.slice(offset, end);
10641
10996
  for (let j = 0, len = limitedResults.length; j < len; j++) {
10642
10997
  yield limitedResults[j];
10643
10998
  }
10644
10999
  } else {
10645
11000
  let yieldedCount = 0;
10646
- let i = offset;
10647
- let nextChunkPromise = null;
10648
- if (yieldedCount < limit && i < totalKeys) {
10649
- const endIdx = Math.min(i + currentChunkSize, totalKeys);
10650
- const chunkKeys = keys.subarray(i, endIdx);
10651
- nextChunkPromise = self.selectMany(chunkKeys, false, tx2);
10652
- i = endIdx;
10653
- }
10654
- while (nextChunkPromise) {
10655
- const rawResults = await nextChunkPromise;
10656
- nextChunkPromise = null;
10657
- const { verySmallChunkSize, smallChunkSize } = self.getFreeMemoryChunkSize();
10658
- if (yieldedCount < limit && i < totalKeys) {
10659
- const endIdx = Math.min(i + currentChunkSize, totalKeys);
10660
- const chunkKeys = keys.subarray(i, endIdx);
10661
- nextChunkPromise = self.selectMany(chunkKeys, false, tx2);
10662
- i = endIdx;
10663
- }
10664
- let chunkTotalSize = 0;
10665
- for (let j = 0, len = rawResults.length; j < len; j++) {
10666
- const s = rawResults[j];
10667
- if (s) {
10668
- if (yieldedCount < limit) {
10669
- yield JSON.parse(s);
10670
- yieldedCount++;
10671
- }
10672
- chunkTotalSize += s.length * 2;
10673
- }
10674
- }
10675
- if (chunkTotalSize > 0) {
10676
- if (chunkTotalSize < verySmallChunkSize) {
10677
- currentChunkSize = currentChunkSize * 2;
10678
- } else if (chunkTotalSize > smallChunkSize) {
10679
- currentChunkSize = Math.max(Math.floor(currentChunkSize / 2), 20);
10680
- }
10681
- }
11001
+ for await (const { doc } of self.processChunkedKeys(
11002
+ keys,
11003
+ offset,
11004
+ self.options.pageSize,
11005
+ ftsConditions,
11006
+ tx2
11007
+ )) {
11008
+ if (yieldedCount >= limit) break;
11009
+ yield doc;
11010
+ yieldedCount++;
10682
11011
  }
10683
11012
  }
10684
11013
  }, tx);
@@ -35,6 +35,11 @@ export declare class DocumentDataplyAPI<T extends DocumentJSON, IC extends Index
35
35
  getDocumentMetadata(tx: Transaction): Promise<DocumentDataplyMetadata>;
36
36
  getDocumentInnerMetadata(tx: Transaction): Promise<DocumentDataplyInnerMetadata>;
37
37
  updateDocumentInnerMetadata(metadata: DocumentDataplyInnerMetadata, tx: Transaction): Promise<void>;
38
+ /**
39
+ * Transforms a query object into a verbose query object
40
+ * @param query The query object to transform
41
+ * @returns The verbose query object
42
+ */
38
43
  verboseQuery<U extends Partial<DocumentDataplyIndexedQuery<T, IC>>, V extends DataplyTreeValue<U>>(query: Partial<DocumentDataplyQuery<U>>): Partial<DocumentDataplyQuery<V>>;
39
44
  /**
40
45
  * Get the selectivity candidate for the given query
@@ -47,11 +52,15 @@ export declare class DocumentDataplyAPI<T extends DocumentJSON, IC extends Index
47
52
  tree: BPTreeAsync<number, V>;
48
53
  condition: Partial<DocumentDataplyCondition<U>>;
49
54
  field: string;
55
+ isFtsMatch?: boolean;
56
+ matchTokens?: string[];
50
57
  };
51
58
  others: {
52
59
  tree: BPTreeAsync<number, V>;
53
60
  condition: Partial<DocumentDataplyCondition<U>>;
54
61
  field: string;
62
+ isFtsMatch?: boolean;
63
+ matchTokens?: string[];
55
64
  }[];
56
65
  rollback: () => void;
57
66
  } | null>;
@@ -63,9 +72,15 @@ export declare class DocumentDataplyAPI<T extends DocumentJSON, IC extends Index
63
72
  verySmallChunkSize: number;
64
73
  smallChunkSize: number;
65
74
  };
75
+ private getTokenKey;
76
+ private applyCandidateByFTS;
66
77
  /**
67
- * Get Primary Keys based on query and index selection.
68
- * Internal common method to unify query optimization.
78
+ * 특정 인덱스 후보를 조회하여 PK 집합을 필터링합니다.
79
+ */
80
+ private applyCandidate;
81
+ /**
82
+ * 쿼리와 인덱스 선택을 기반으로 기본 키(Primary Keys)를 가져옵니다.
83
+ * 쿼리 최적화를 통합하기 위한 내부 공통 메서드입니다.
69
84
  */
70
85
  getKeys(query: Partial<DocumentDataplyIndexedQuery<T, IC>>, orderBy?: keyof IC | '_id', sortOrder?: 'asc' | 'desc'): Promise<Float64Array>;
71
86
  private insertDocumentInternal;
@@ -121,6 +136,19 @@ export declare class DocumentDataplyAPI<T extends DocumentJSON, IC extends Index
121
136
  * @returns The number of documents that match the query
122
137
  */
123
138
  countDocuments(query: Partial<DocumentDataplyIndexedQuery<T, IC>>, tx?: Transaction): Promise<number>;
139
+ /**
140
+ * FTS 조건에 대해 문서가 유효한지 검증합니다.
141
+ */
142
+ private verifyFts;
143
+ /**
144
+ * 메모리 기반으로 청크 크기를 동적 조절합니다.
145
+ */
146
+ private adjustChunkSize;
147
+ /**
148
+ * Prefetch 방식으로 키 배열을 청크 단위로 조회하여 문서를 순회합니다.
149
+ * FTS 검증을 통과한 문서만 yield 합니다.
150
+ */
151
+ private processChunkedKeys;
124
152
  /**
125
153
  * Select documents from the database
126
154
  * @param query The query to use (only indexed fields + _id allowed)
@@ -16,7 +16,17 @@ export interface DocumentDataplyInnerMetadata {
16
16
  updatedAt: number;
17
17
  lastId: number;
18
18
  indices: {
19
- [key: string]: [number, boolean];
19
+ [key: string]: [
20
+ number,
21
+ boolean | {
22
+ type: 'fts';
23
+ tokenizer: 'whitespace';
24
+ } | {
25
+ type: 'fts';
26
+ tokenizer: 'ngram';
27
+ gramSize: number;
28
+ }
29
+ ];
20
30
  };
21
31
  }
22
32
  export interface DocumentDataplyMetadata {
@@ -46,6 +56,7 @@ export type DocumentDataplyCondition<V> = {
46
56
  notEqual?: Partial<V>;
47
57
  or?: Partial<V>[];
48
58
  like?: string;
59
+ match?: string;
49
60
  };
50
61
  export type DocumentDataplyQuery<T> = {
51
62
  [key in keyof T]?: T[key] | DocumentDataplyCondition<T[key]>;
@@ -110,10 +121,18 @@ export type DocumentDataplyIndices<T extends DocumentJSON, IC extends IndexConfi
110
121
  [key in keyof IC & keyof FinalFlatten<T>]: GetTypeByPath<T, key>;
111
122
  };
112
123
  /**
113
- * Index configuration type - keys are field names, values are boolean
124
+ * Index configuration type
114
125
  */
126
+ export type FTSConfig = {
127
+ type: 'fts';
128
+ tokenizer: 'whitespace';
129
+ } | {
130
+ type: 'fts';
131
+ tokenizer: 'ngram';
132
+ gramSize: number;
133
+ };
115
134
  export type IndexConfig<T> = Partial<{
116
- [key in keyof FinalFlatten<T>]: boolean;
135
+ [key in keyof FinalFlatten<T>]: boolean | FTSConfig;
117
136
  }>;
118
137
  /**
119
138
  * Extract index keys from IndexConfig
@@ -0,0 +1 @@
1
+ export declare function fastStringHash(str: string): number;
@@ -0,0 +1,4 @@
1
+ import { FTSConfig } from '../types';
2
+ export declare function whitespaceTokenize(text: string): string[];
3
+ export declare function ngramTokenize(text: string, gramSize: number): string[];
4
+ export declare function tokenize(text: string, options: FTSConfig): string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-dataply",
3
- "version": "0.0.6",
3
+ "version": "0.0.7-alpha.1",
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.22"
45
+ "dataply": "^0.0.23-alpha.2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/jest": "^30.0.0",