serializable-bptree 6.0.1 → 6.1.0

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/README.md CHANGED
@@ -109,386 +109,27 @@ import {
109
109
  </script>
110
110
  ```
111
111
 
112
+ ## Documentation
113
+
114
+ Explore the detailed guides and concepts of `serializable-bptree`:
115
+
116
+ - **Core Concepts**
117
+ - [Value Comparators](./docs/COMPARATORS.md): How sorting and matching works.
118
+ - [Serialize Strategies](./docs/STRATEGIES.md): How to persist nodes to storage.
119
+ - **API & Usage**
120
+ - [Query Conditions](./docs/QUERY.md): Detailed explanation of the `where()` operators.
121
+ - [Asynchronous Usage](./docs/ASYNC.md): How to use the tree in an async environment.
122
+ - **Advanced Topics**
123
+ - [Duplicate Value Handling](./docs/DUPLICATE_VALUES.md): Strategies for managing large amounts of duplicate data.
124
+ - [Concurrency & Synchronization](./docs/CONCURRENCY.md): Multi-instance usage and locking mechanisms.
125
+
112
126
  ## Migration from v5.x.x to v6.0.0
113
127
 
114
128
  Version 6.0.0 includes a critical fix for how internal nodes are sorted.
115
129
 
116
130
  > [!IMPORTANT]
117
131
  > **Breaking Changes & Incompatibility**
118
- > In previous versions, internal nodes were not strictly sorted by value magnitude, which could lead to incorrect traversals and failed queries (especially for the last nodes in a branch).
119
- > v6.0.0 enforces strict value sorting. **Data structures created with v5.x.x or earlier may be incompatible** with v6.0.0 if they contain unsorted internal nodes. It is highly recommended to rebuild your tree from scratch when upgrading.
120
-
121
- ## Conceptualization
122
-
123
- ### Value comparator
124
-
125
- B+tree needs to keep values in sorted order. Therefore, a process to compare the sizes of values is needed, and that role is played by the **ValueComparator**.
126
-
127
- Commonly used numerical and string comparisons are natively supported by the **serializable-bptree** library. Use it as follows:
128
-
129
- ```typescript
130
- import { NumericComparator, StringComparator } from 'serializable-bptree'
131
- ```
132
-
133
- However, you may want to sort complex objects other than numbers and strings. For example, if you want to sort by the **age** property order of an object, you need to create a new class that inherits from the **ValueComparator** class. Use it as follows:
134
-
135
- ```typescript
136
- import { ValueComparator } from 'serializable-bptree'
137
-
138
- interface MyObject {
139
- age: number
140
- name: string
141
- }
142
-
143
- class AgeComparator extends ValueComparator<MyObject> {
144
- asc(a: MyObject, b: MyObject): number {
145
- return a.age - b.age
146
- }
147
-
148
- match(value: MyObject): string {
149
- return value.age.toString()
150
- }
151
- }
152
- ```
153
-
154
- #### asc
155
-
156
- The **asc** method should return values in ascending order. If the return value is negative, it means that the parameter **a** is smaller than **b**. If the return value is positive, it means that **a** is greater than **b**. If the return value is **0**, it indicates that **a** and **b** are of the same size.
157
-
158
- #### match
159
-
160
- The `match` method is used for the **LIKE** operator. This method specifies which value to test against a regular expression. For example, if you have a tree with values of the structure `{ country: string, capital: string }`, and you want to perform a **LIKE** operation based on the **capital** value, the method should return **value.capital**. In this case, you **CANNOT** perform a **LIKE** operation based on the **country** attribute. The returned value must be a string.
161
-
162
- ```typescript
163
- interface MyObject {
164
- country: string
165
- capital: string
166
- }
167
-
168
- class CompositeComparator extends ValueComparator<MyObject> {
169
- ...
170
- match(value: MyObject): string {
171
- return value.capital
172
- }
173
- }
174
- ```
175
-
176
- For a tree with simple structure, without complex nesting, returning the value directly would be sufficient.
177
-
178
- ```typescript
179
- class StringComparator extends ValueComparator<string> {
180
- match(value: string): string {
181
- return value
182
- }
183
- }
184
- ```
185
-
186
- ### Serialize strategy
187
-
188
- A B+tree instance is made up of numerous nodes. You would want to store this value when such nodes are created or updated. Let's assume you want to save it to a file.
189
-
190
- You need to construct a logic for input/output from the file by inheriting the SerializeStrategy class. Look at the class structure below:
191
-
192
- ```typescript
193
- import { SerializeStrategySync } from 'serializable-bptree'
194
-
195
- class MyFileIOStrategySync extends SerializeStrategySync {
196
- id(): string
197
- read(id: string): BPTreeNode<K, V>
198
- write(id: string, node: BPTreeNode<K, V>): void
199
- delete(id: string): void
200
- readHead(): SerializeStrategyHead|null
201
- writeHead(head: SerializeStrategyHead): void
202
- }
203
- ```
204
-
205
- What does this method mean? And why do we need to construct such a method?
206
-
207
- #### id(isLeaf: `boolean`): `string`
208
-
209
- When a node is created in the B+tree, the node needs a unique value to represent itself. This is the **node.id** attribute, and you can specify this attribute yourself.
210
-
211
- Typically, such an **id** value can be a unique string like a UUID. Below is an example of usage:
212
-
213
- ```typescript
214
- id(isLeaf: boolean): string {
215
- return crypto.randomUUID()
216
- }
217
- ```
218
-
219
- The **id** method is called before a node is created in the tree. Therefore, it can also be used to allocate space for storing the node.
220
-
221
- #### read(id: `string`): `BPTreeNode<K, V>`
222
-
223
- This is a method to load the saved value as a tree instance. If you have previously saved the node as a file, you should use this method to convert it back to JavaScript JSON format and return it.
224
-
225
- Please refer to the example below:
226
-
227
- ```typescript
228
- read(id: string): BPTreeNode<K, V> {
229
- const filePath = `./my-store/${id}`
230
- const raw = fs.readFileSync(filePath, 'utf8')
231
- return JSON.parse(raw)
232
- }
233
- ```
234
-
235
- This method is called only once when loading a node from a tree instance. The loaded node is loaded into memory, and subsequently, when the tree references the node, it operates based on the values in memory **without** re-invoking this method.
236
-
237
- #### write(id: `string`, node: `BPTreeNode<K, V>`): `void`
238
-
239
- This method is called when there are changes in the internal nodes due to the insert or delete operations of the tree instance. In other words, it's a necessary method for synchronizing the in-memory nodes into a file.
240
-
241
- Since this method is called frequently, be mindful of performance. There are ways to optimize it using a write-back caching technique.
242
-
243
- Please refer to the example below:
244
-
245
- ```typescript
246
- let queue = 0
247
- function writeBack(id: string, node: BPTreeNode<K, V>, timer: number) {
248
- clearTimeout(queue)
249
- queue = setTimeout(() => {
250
- const filePath = `./my-store/${id}`
251
- const stringify = JSON.stringify(node)
252
- writeFileSync(filePath, stringify, 'utf8')
253
- }, timer)
254
- }
255
-
256
- ...
257
- write(id: string, node: BPTreeNode<K, V>): void {
258
- const writeBackInterval = 10
259
- writeBack(id, node, writeBackInterval)
260
- }
261
- ```
262
-
263
- This kind of delay writing should ideally occur within a few milliseconds. If this is not feasible, consider other approaches.
264
-
265
- #### delete(id: `string`): `void`
266
-
267
- This method is called when previously created nodes become no longer needed due to deletion or other processes. It can be used to free up space by deleting existing stored nodes.
268
-
269
- ```typescript
270
- delete(id: string): void {
271
- const filePath = `./my-store/${id}`
272
- fs.unlinkSync(filePath)
273
- }
274
- ```
275
-
276
- #### readHead(): `SerializeStrategyHead`|`null`
277
-
278
- This method is called only once when the tree is created. It's a method to restore the saved tree information. If it is the initial creation and there is no stored root node, it should return **null**.
279
-
280
- This method should return the value stored in the **writeHead** method.
281
-
282
- #### writeHead(head: `SerializeStrategyHead`): `void`
283
-
284
- This method is called whenever the head information of the tree changes, typically when the root node changes. This method also works when the tree's **setHeadData** method is called. This is because the method attempts to store head data in the root node.
285
-
286
- As a parameter, it receives the header information of the tree. This value should be serialized and stored. Later, the **readHead** method should convert this serialized value into a json format and return it.
287
-
288
- ### The Default `ValueComparator` and `SerializeStrategy`
289
-
290
- To utilize **serializable-bptree**, you need to implement certain functions. However, a few basic helper classes are provided by default.
291
-
292
- #### ValueComparator
293
-
294
- * `NumericComparator`
295
- * `StringComparator`
296
-
297
- If the values being inserted into the tree are numeric, please use the **NumericComparator** class.
298
-
299
- ```typescript
300
- import { NumericComparator } from 'serializable-bptree'
301
- ```
302
-
303
- If the values being inserted into the tree can be strings, you can use the **StringComparator** class in this case.
304
-
305
- ```typescript
306
- import { StringComparator } from 'serializable-bptree'
307
- ```
308
-
309
- #### SerializeStrategy
310
-
311
- * `InMemoryStoreStrategySync`
312
- * `InMemoryStoreStrategyAsync`
313
-
314
- As of now, the only class supported by default is the **InMemoryStoreStrategy**. This class is suitable for use when you prefer to operate the tree solely in-memory, similar to a typical B+ tree.
315
-
316
- ```typescript
317
- import {
318
- InMemoryStoreStrategySync,
319
- InMemoryStoreStrategyAsync
320
- } from 'serializable-bptree'
321
- ```
322
-
323
- ## Data Query Condition Clause
324
-
325
- This library supports various conditional clauses. Currently, it supports **gte**, **gt**, **lte**, **lt**, **equal**, **notEqual**, **or**, and **like** conditions. Each condition is as follows:
326
-
327
- ### `gte`
328
-
329
- Queries values that are greater than or equal to the given value.
330
-
331
- ```typescript
332
- tree.where({ gte: 1 })
333
- ```
334
-
335
- ### `gt`
336
-
337
- Queries values that are greater than the given value.
338
-
339
- ```typescript
340
- tree.where({ gt: 1 })
341
- ```
342
-
343
- ### `lte`
344
-
345
- Queries values that are less than or equal to the given value.
346
-
347
- ```typescript
348
- tree.where({ lte: 5 })
349
- ```
350
-
351
- ### `lt`
352
-
353
- Queries values that are less than the given value.
354
-
355
- ```typescript
356
- tree.where({ lt: 5 })
357
- ```
358
-
359
- ### `equal`
360
-
361
- Queries values that match the given value.
362
-
363
- ```typescript
364
- tree.where({ equal: 3 })
365
- ```
366
-
367
- ### `notEqual`
368
-
369
- Queries values that do not match the given value.
370
-
371
- ```typescript
372
- tree.where({ notEqual: 3 })
373
- ```
374
-
375
- ### `or`
376
-
377
- Queries values that satisfy at least one of the given conditions. It accepts an array of conditions, and if any of these conditions are met, the data is included in the result.
378
-
379
- ```typescript
380
- tree.where({ or: [1, 2, 3] })
381
- ```
382
-
383
- ### `like`
384
-
385
- Queries values that contain the given value in a manner similar to regular expressions. Special characters such as % and _ can be used.
386
-
387
- **%** matches zero or more characters. For example, **%ada%** means all strings that contain "ada" anywhere in the string. **%ada** means strings that end with "ada". **ada%** means strings that start with **"ada"**.
388
-
389
- **_** matches exactly one character.
390
- Using **p_t**, it can match any string where the underscore is replaced by any character, such as "pit", "put", etc.
391
-
392
- You can obtain matching data by combining these condition clauses. If there are multiple conditions, an **AND** operation is used to retrieve only the data that satisfies all conditions.
393
-
394
- ```typescript
395
- tree.where({ like: 'hello%' })
396
- tree.where({ like: 'he__o%' })
397
- tree.where({ like: '%world!' })
398
- tree.where({ like: '%lo, wor%' })
399
- ```
400
-
401
- ## Using Asynchronously
402
-
403
- Support for asynchronous trees has been available since version 3.0.0. Asynchronous is useful for operations with delays, such as file input/output and remote storage. Here is an example of how to use it:
404
-
405
- ```typescript
406
- import { existsSync } from 'fs'
407
- import { readFile, writeFile, unlink } from 'fs/promises'
408
- import {
409
- BPTreeAsync,
410
- SerializeStrategyAsync,
411
- NumericComparator,
412
- StringComparator
413
- } from 'serializable-bptree'
414
-
415
- class FileStoreStrategyAsync extends SerializeStrategyAsync<K, V> {
416
- async id(isLeaf: boolean): Promise<string> {
417
- return crypto.randomUUID()
418
- }
419
-
420
- async read(id: string): Promise<BPTreeNode<K, V>> {
421
- const raw = await readFile(id, 'utf8')
422
- return JSON.parse(raw)
423
- }
424
-
425
- async write(id: string, node: BPTreeNode<K, V>): Promise<void> {
426
- const stringify = JSON.stringify(node)
427
- await writeFile(id, stringify, 'utf8')
428
- }
429
-
430
- async delete(id: string): Promise<void> {
431
- await unlink(id)
432
- }
433
-
434
- async readHead(): Promise<SerializeStrategyHead|null> {
435
- if (!existsSync('head')) {
436
- return null
437
- }
438
- const raw = await readFile('head', 'utf8')
439
- return JSON.parse(raw)
440
- }
441
-
442
- async writeHead(head: SerializeStrategyHead): Promise<void> {
443
- const stringify = JSON.stringify(head)
444
- await writeFile('head', stringify, 'utf8')
445
- }
446
- }
447
-
448
- const order = 5
449
- const tree = new BPTreeAsync(
450
- new FileStoreStrategyAsync(order),
451
- new NumericComparator()
452
- )
453
-
454
- await tree.init()
455
- await tree.insert('a', 1)
456
- await tree.insert('b', 2)
457
- await tree.insert('c', 3)
458
-
459
- await tree.delete('b', 2)
460
-
461
- await tree.where({ equal: 1 }) // Map([{ key: 'a', value: 1 }])
462
- await tree.where({ gt: 1 }) // Map([{ key: 'c', value: 3 }])
463
- await tree.where({ lt: 2 }) // Map([{ key: 'a', value: 1 }])
464
- await tree.where({ gt: 0, lt: 4 }) // Map([{ key: 'a', value: 1 }, { key: 'c', value: 3 }])
465
-
466
- tree.clear()
467
- ```
468
-
469
- The implementation method for asynchronous operations is not significantly different. The **-Async** suffix is used instead of the **-Sync** suffix in the **BPTree** and **SerializeStrategy** classes. The only difference is that the methods become asynchronous. The **ValueComparator** class and similar value comparators do not use asynchronous operations.
470
-
471
- ## Precautions for Use
472
-
473
- ### Synchronization Issue
474
-
475
- The serializable-bptree minimizes file I/O by storing loaded nodes in-memory (caching). This approach works perfectly when a single tree instance is used for a given storage.
476
-
477
- However, if **multiple BPTree instances** (e.g., across different processes or servers) read from and write to a **single shared storage**, data inconsistency can occur. This is because each instance maintains its own independent in-memory cache, and changes made by one instance are not automatically reflected in the others.
478
-
479
- To solve this problem, you must synchronize the cached nodes across all instances. The `forceUpdate` method can be used to refresh the nodes cached in a tree instance. When one instance saves data to the shared storage, you should implement a signaling mechanism (e.g., via Pub/Sub or WebSockets) to notify other instances that a node has been updated. Upon receiving this signal, the other instances should call the `forceUpdate` method to ensure they are working with the latest data.
480
-
481
- ### Concurrency Issue in Asynchronous Trees
482
-
483
- This issue occurs only in asynchronous trees and can also occur in a 1:1 relationship between remote storage and client.
484
-
485
- Since version 5.x.x, **serializable-bptree** provides a built-in read/write lock for the `BPTreeAsync` class to prevent data inconsistency during concurrent operations. Calling public methods like `insert`, `delete`, `where`, `exists`, `keys`, `setHeadData`, and `forceUpdate` will automatically acquire the appropriate lock.
486
-
487
- However, please be aware of the following technical limitations:
488
- - **Locking is only applied to public methods**: The internal `protected` methods (e.g., `_insertInParent`, `_deleteEntry`, etc.) do not automatically acquire locks.
489
- - **Inheritance Caution**: If you extend the `BPTreeAsync` class and call `protected` methods directly, you must manually manage the locks using `readLock` or `writeLock` to ensure data integrity.
490
-
491
- Despite these safeguards, it is still recommended to avoid unnecessary concurrent operations whenever possible to maintain optimal performance and predictability.
132
+ > v6.0.0 enforces strict value sorting. **Data structures created with v5.x.x or earlier are incompatible** with v6.0.0. It is highly recommended to rebuild your tree from scratch. For more details, see the [Concurrency & Synchronization](./docs/CONCURRENCY.md) guide.
492
133
 
493
134
  ## LICENSE
494
135
 
@@ -43,6 +43,33 @@ var ValueComparator = class {
43
43
  isHigher(value, than) {
44
44
  return this.asc(value, than) > 0;
45
45
  }
46
+ /**
47
+ * This method is used for range queries with composite values.
48
+ * By default, it calls the `asc` method, so existing code works without changes.
49
+ *
50
+ * When using composite values (e.g., `{ k: number, v: number }`),
51
+ * override this method to compare only the primary sorting field (e.g., `v`),
52
+ * ignoring the unique identifier field (e.g., `k`).
53
+ *
54
+ * This enables efficient range queries like `primaryEqual` that find all entries
55
+ * with the same primary value regardless of their unique identifiers.
56
+ *
57
+ * @param a Value a.
58
+ * @param b Value b.
59
+ * @returns Negative if a < b, 0 if equal, positive if a > b (based on primary field only).
60
+ */
61
+ primaryAsc(a, b) {
62
+ return this.asc(a, b);
63
+ }
64
+ isPrimarySame(value, than) {
65
+ return this.primaryAsc(value, than) === 0;
66
+ }
67
+ isPrimaryLower(value, than) {
68
+ return this.primaryAsc(value, than) < 0;
69
+ }
70
+ isPrimaryHigher(value, than) {
71
+ return this.primaryAsc(value, than) > 0;
72
+ }
46
73
  };
47
74
  var NumericComparator = class extends ValueComparator {
48
75
  asc(a, b) {
@@ -456,6 +483,7 @@ var BPTree = class {
456
483
  lt: (nv, v) => this.comparator.isLower(nv, v),
457
484
  lte: (nv, v) => this.comparator.isLower(nv, v) || this.comparator.isSame(nv, v),
458
485
  equal: (nv, v) => this.comparator.isSame(nv, v),
486
+ primaryEqual: (nv, v) => this.comparator.isPrimarySame(nv, v),
459
487
  notEqual: (nv, v) => this.comparator.isSame(nv, v) === false,
460
488
  or: (nv, v) => this.ensureValues(v).some((v2) => this.comparator.isSame(nv, v2)),
461
489
  like: (nv, v) => {
@@ -472,6 +500,7 @@ var BPTree = class {
472
500
  lt: (v) => this.insertableNode(v),
473
501
  lte: (v) => this.insertableNode(v),
474
502
  equal: (v) => this.insertableNode(v),
503
+ primaryEqual: (v) => this.insertableNodeByPrimary(v),
475
504
  notEqual: (v) => this.leftestNode(),
476
505
  or: (v) => this.insertableNode(this.lowestValue(this.ensureValues(v))),
477
506
  like: (v) => this.leftestNode()
@@ -482,6 +511,7 @@ var BPTree = class {
482
511
  lt: (v) => null,
483
512
  lte: (v) => null,
484
513
  equal: (v) => this.insertableEndNode(v, this.verifierDirection.equal),
514
+ primaryEqual: (v) => null,
485
515
  notEqual: (v) => null,
486
516
  or: (v) => this.insertableEndNode(
487
517
  this.highestValue(this.ensureValues(v)),
@@ -495,10 +525,27 @@ var BPTree = class {
495
525
  lt: -1,
496
526
  lte: -1,
497
527
  equal: 1,
528
+ primaryEqual: 1,
498
529
  notEqual: 1,
499
530
  or: 1,
500
531
  like: 1
501
532
  };
533
+ /**
534
+ * Determines whether early termination is allowed for each condition.
535
+ * When true, the search will stop once a match is found and then a non-match is encountered.
536
+ * Only applicable for conditions that guarantee contiguous matches in a sorted B+Tree.
537
+ */
538
+ verifierEarlyTerminate = {
539
+ gt: false,
540
+ gte: false,
541
+ lt: false,
542
+ lte: false,
543
+ equal: true,
544
+ primaryEqual: true,
545
+ notEqual: false,
546
+ or: false,
547
+ like: false
548
+ };
502
549
  constructor(strategy, comparator, option) {
503
550
  this.strategy = strategy;
504
551
  this.comparator = comparator;
@@ -611,10 +658,11 @@ var BPTreeSync = class extends BPTree {
611
658
  capacity: this.option.capacity ?? 1e3
612
659
  });
613
660
  }
614
- getPairsRightToLeft(value, startNode, endNode, comparator) {
661
+ getPairsRightToLeft(value, startNode, endNode, comparator, earlyTerminate) {
615
662
  const pairs = [];
616
663
  let node = startNode;
617
664
  let done = false;
665
+ let hasMatched = false;
618
666
  while (!done) {
619
667
  if (endNode && node.id === endNode.id) {
620
668
  done = true;
@@ -625,12 +673,17 @@ var BPTreeSync = class extends BPTree {
625
673
  const nValue = node.values[i];
626
674
  const keys = node.keys[i];
627
675
  if (comparator(nValue, value)) {
676
+ hasMatched = true;
628
677
  let j = keys.length;
629
678
  while (j--) {
630
679
  pairs.push([keys[j], nValue]);
631
680
  }
681
+ } else if (earlyTerminate && hasMatched) {
682
+ done = true;
683
+ break;
632
684
  }
633
685
  }
686
+ if (done) break;
634
687
  if (!node.prev) {
635
688
  done = true;
636
689
  break;
@@ -639,10 +692,11 @@ var BPTreeSync = class extends BPTree {
639
692
  }
640
693
  return new Map(pairs.reverse());
641
694
  }
642
- getPairsLeftToRight(value, startNode, endNode, comparator) {
695
+ getPairsLeftToRight(value, startNode, endNode, comparator, earlyTerminate) {
643
696
  const pairs = [];
644
697
  let node = startNode;
645
698
  let done = false;
699
+ let hasMatched = false;
646
700
  while (!done) {
647
701
  if (endNode && node.id === endNode.id) {
648
702
  done = true;
@@ -652,12 +706,17 @@ var BPTreeSync = class extends BPTree {
652
706
  const nValue = node.values[i];
653
707
  const keys = node.keys[i];
654
708
  if (comparator(nValue, value)) {
709
+ hasMatched = true;
655
710
  for (let j = 0, len2 = keys.length; j < len2; j++) {
656
711
  const key = keys[j];
657
712
  pairs.push([key, nValue]);
658
713
  }
714
+ } else if (earlyTerminate && hasMatched) {
715
+ done = true;
716
+ break;
659
717
  }
660
718
  }
719
+ if (done) break;
661
720
  if (!node.next) {
662
721
  done = true;
663
722
  break;
@@ -666,12 +725,12 @@ var BPTreeSync = class extends BPTree {
666
725
  }
667
726
  return new Map(pairs);
668
727
  }
669
- getPairs(value, startNode, endNode, comparator, direction) {
728
+ getPairs(value, startNode, endNode, comparator, direction, earlyTerminate) {
670
729
  switch (direction) {
671
730
  case -1:
672
- return this.getPairsRightToLeft(value, startNode, endNode, comparator);
731
+ return this.getPairsRightToLeft(value, startNode, endNode, comparator, earlyTerminate);
673
732
  case 1:
674
- return this.getPairsLeftToRight(value, startNode, endNode, comparator);
733
+ return this.getPairsLeftToRight(value, startNode, endNode, comparator, earlyTerminate);
675
734
  default:
676
735
  throw new Error(`Direction must be -1 or 1. but got a ${direction}`);
677
736
  }
@@ -991,6 +1050,9 @@ var BPTreeSync = class extends BPTree {
991
1050
  }
992
1051
  }
993
1052
  getNode(id) {
1053
+ if (this._nodeUpdateBuffer.has(id)) {
1054
+ return this._nodeUpdateBuffer.get(id);
1055
+ }
994
1056
  if (this._nodeCreateBuffer.has(id)) {
995
1057
  return this._nodeCreateBuffer.get(id);
996
1058
  }
@@ -998,7 +1060,7 @@ var BPTreeSync = class extends BPTree {
998
1060
  return cache.raw;
999
1061
  }
1000
1062
  insertableNode(value) {
1001
- let node = this.root;
1063
+ let node = this.getNode(this.root.id);
1002
1064
  while (!node.leaf) {
1003
1065
  for (let i = 0, len = node.values.length; i < len; i++) {
1004
1066
  const nValue = node.values[i];
@@ -1017,6 +1079,30 @@ var BPTreeSync = class extends BPTree {
1017
1079
  }
1018
1080
  return node;
1019
1081
  }
1082
+ /**
1083
+ * Find the insertable node using primaryAsc comparison.
1084
+ * This allows finding nodes by primary value only, ignoring unique identifiers.
1085
+ */
1086
+ insertableNodeByPrimary(value) {
1087
+ let node = this.getNode(this.root.id);
1088
+ while (!node.leaf) {
1089
+ for (let i = 0, len = node.values.length; i < len; i++) {
1090
+ const nValue = node.values[i];
1091
+ const k = node.keys;
1092
+ if (this.comparator.isPrimarySame(value, nValue)) {
1093
+ node = this.getNode(k[i]);
1094
+ break;
1095
+ } else if (this.comparator.isPrimaryLower(value, nValue)) {
1096
+ node = this.getNode(k[i]);
1097
+ break;
1098
+ } else if (i + 1 === node.values.length) {
1099
+ node = this.getNode(k[i + 1]);
1100
+ break;
1101
+ }
1102
+ }
1103
+ }
1104
+ return node;
1105
+ }
1020
1106
  insertableEndNode(value, direction) {
1021
1107
  const insertableNode = this.insertableNode(value);
1022
1108
  let key;
@@ -1085,7 +1171,8 @@ var BPTreeSync = class extends BPTree {
1085
1171
  const endNode = this.verifierEndNode[key](value);
1086
1172
  const direction = this.verifierDirection[key];
1087
1173
  const comparator = this.verifierMap[key];
1088
- const pairs = this.getPairs(value, startNode, endNode, comparator, direction);
1174
+ const earlyTerminate = this.verifierEarlyTerminate[key];
1175
+ const pairs = this.getPairs(value, startNode, endNode, comparator, direction, earlyTerminate);
1089
1176
  if (!filterValues) {
1090
1177
  filterValues = new Set(pairs.keys());
1091
1178
  } else {
@@ -1110,7 +1197,8 @@ var BPTreeSync = class extends BPTree {
1110
1197
  const endNode = this.verifierEndNode[key](value);
1111
1198
  const direction = this.verifierDirection[key];
1112
1199
  const comparator = this.verifierMap[key];
1113
- const pairs = this.getPairs(value, startNode, endNode, comparator, direction);
1200
+ const earlyTerminate = this.verifierEarlyTerminate[key];
1201
+ const pairs = this.getPairs(value, startNode, endNode, comparator, direction, earlyTerminate);
1114
1202
  if (result === null) {
1115
1203
  result = pairs;
1116
1204
  } else {
@@ -1498,10 +1586,11 @@ var BPTreeAsync = class extends BPTree {
1498
1586
  this.lock.writeUnlock(lockId);
1499
1587
  });
1500
1588
  }
1501
- async getPairsRightToLeft(value, startNode, endNode, comparator) {
1589
+ async getPairsRightToLeft(value, startNode, endNode, comparator, earlyTerminate) {
1502
1590
  const pairs = [];
1503
1591
  let node = startNode;
1504
1592
  let done = false;
1593
+ let hasMatched = false;
1505
1594
  while (!done) {
1506
1595
  if (endNode && node.id === endNode.id) {
1507
1596
  done = true;
@@ -1512,12 +1601,17 @@ var BPTreeAsync = class extends BPTree {
1512
1601
  const nValue = node.values[i];
1513
1602
  const keys = node.keys[i];
1514
1603
  if (comparator(nValue, value)) {
1604
+ hasMatched = true;
1515
1605
  let j = keys.length;
1516
1606
  while (j--) {
1517
1607
  pairs.push([keys[j], nValue]);
1518
1608
  }
1609
+ } else if (earlyTerminate && hasMatched) {
1610
+ done = true;
1611
+ break;
1519
1612
  }
1520
1613
  }
1614
+ if (done) break;
1521
1615
  if (!node.prev) {
1522
1616
  done = true;
1523
1617
  break;
@@ -1526,10 +1620,11 @@ var BPTreeAsync = class extends BPTree {
1526
1620
  }
1527
1621
  return new Map(pairs.reverse());
1528
1622
  }
1529
- async getPairsLeftToRight(value, startNode, endNode, comparator) {
1623
+ async getPairsLeftToRight(value, startNode, endNode, comparator, earlyTerminate) {
1530
1624
  const pairs = [];
1531
1625
  let node = startNode;
1532
1626
  let done = false;
1627
+ let hasMatched = false;
1533
1628
  while (!done) {
1534
1629
  if (endNode && node.id === endNode.id) {
1535
1630
  done = true;
@@ -1539,12 +1634,17 @@ var BPTreeAsync = class extends BPTree {
1539
1634
  const nValue = node.values[i];
1540
1635
  const keys = node.keys[i];
1541
1636
  if (comparator(nValue, value)) {
1637
+ hasMatched = true;
1542
1638
  for (let j = 0, len2 = keys.length; j < len2; j++) {
1543
1639
  const key = keys[j];
1544
1640
  pairs.push([key, nValue]);
1545
1641
  }
1642
+ } else if (earlyTerminate && hasMatched) {
1643
+ done = true;
1644
+ break;
1546
1645
  }
1547
1646
  }
1647
+ if (done) break;
1548
1648
  if (!node.next) {
1549
1649
  done = true;
1550
1650
  break;
@@ -1553,12 +1653,12 @@ var BPTreeAsync = class extends BPTree {
1553
1653
  }
1554
1654
  return new Map(pairs);
1555
1655
  }
1556
- async getPairs(value, startNode, endNode, comparator, direction) {
1656
+ async getPairs(value, startNode, endNode, comparator, direction, earlyTerminate) {
1557
1657
  switch (direction) {
1558
1658
  case -1:
1559
- return await this.getPairsRightToLeft(value, startNode, endNode, comparator);
1659
+ return await this.getPairsRightToLeft(value, startNode, endNode, comparator, earlyTerminate);
1560
1660
  case 1:
1561
- return await this.getPairsLeftToRight(value, startNode, endNode, comparator);
1661
+ return await this.getPairsLeftToRight(value, startNode, endNode, comparator, earlyTerminate);
1562
1662
  default:
1563
1663
  throw new Error(`Direction must be -1 or 1. but got a ${direction}`);
1564
1664
  }
@@ -1878,6 +1978,9 @@ var BPTreeAsync = class extends BPTree {
1878
1978
  }
1879
1979
  }
1880
1980
  async getNode(id) {
1981
+ if (this._nodeUpdateBuffer.has(id)) {
1982
+ return this._nodeUpdateBuffer.get(id);
1983
+ }
1881
1984
  if (this._nodeCreateBuffer.has(id)) {
1882
1985
  return this._nodeCreateBuffer.get(id);
1883
1986
  }
@@ -1885,7 +1988,7 @@ var BPTreeAsync = class extends BPTree {
1885
1988
  return cache.raw;
1886
1989
  }
1887
1990
  async insertableNode(value) {
1888
- let node = this.root;
1991
+ let node = await this.getNode(this.root.id);
1889
1992
  while (!node.leaf) {
1890
1993
  for (let i = 0, len = node.values.length; i < len; i++) {
1891
1994
  const nValue = node.values[i];
@@ -1904,6 +2007,30 @@ var BPTreeAsync = class extends BPTree {
1904
2007
  }
1905
2008
  return node;
1906
2009
  }
2010
+ /**
2011
+ * Find the insertable node using primaryAsc comparison.
2012
+ * This allows finding nodes by primary value only, ignoring unique identifiers.
2013
+ */
2014
+ async insertableNodeByPrimary(value) {
2015
+ let node = await this.getNode(this.root.id);
2016
+ while (!node.leaf) {
2017
+ for (let i = 0, len = node.values.length; i < len; i++) {
2018
+ const nValue = node.values[i];
2019
+ const k = node.keys;
2020
+ if (this.comparator.isPrimarySame(value, nValue)) {
2021
+ node = await this.getNode(k[i]);
2022
+ break;
2023
+ } else if (this.comparator.isPrimaryLower(value, nValue)) {
2024
+ node = await this.getNode(k[i]);
2025
+ break;
2026
+ } else if (i + 1 === node.values.length) {
2027
+ node = await this.getNode(k[i + 1]);
2028
+ break;
2029
+ }
2030
+ }
2031
+ }
2032
+ return node;
2033
+ }
1907
2034
  async insertableEndNode(value, direction) {
1908
2035
  const insertableNode = await this.insertableNode(value);
1909
2036
  let key;
@@ -1973,7 +2100,8 @@ var BPTreeAsync = class extends BPTree {
1973
2100
  const endNode = await this.verifierEndNode[key](value);
1974
2101
  const direction = this.verifierDirection[key];
1975
2102
  const comparator = this.verifierMap[key];
1976
- const pairs = await this.getPairs(value, startNode, endNode, comparator, direction);
2103
+ const earlyTerminate = this.verifierEarlyTerminate[key];
2104
+ const pairs = await this.getPairs(value, startNode, endNode, comparator, direction, earlyTerminate);
1977
2105
  if (!filterValues) {
1978
2106
  filterValues = new Set(pairs.keys());
1979
2107
  } else {
@@ -2000,7 +2128,8 @@ var BPTreeAsync = class extends BPTree {
2000
2128
  const endNode = await this.verifierEndNode[key](value);
2001
2129
  const direction = this.verifierDirection[key];
2002
2130
  const comparator = this.verifierMap[key];
2003
- const pairs = await this.getPairs(value, startNode, endNode, comparator, direction);
2131
+ const earlyTerminate = this.verifierEarlyTerminate[key];
2132
+ const pairs = await this.getPairs(value, startNode, endNode, comparator, direction, earlyTerminate);
2004
2133
  if (result === null) {
2005
2134
  result = pairs;
2006
2135
  } else {
@@ -9,6 +9,33 @@ var ValueComparator = class {
9
9
  isHigher(value, than) {
10
10
  return this.asc(value, than) > 0;
11
11
  }
12
+ /**
13
+ * This method is used for range queries with composite values.
14
+ * By default, it calls the `asc` method, so existing code works without changes.
15
+ *
16
+ * When using composite values (e.g., `{ k: number, v: number }`),
17
+ * override this method to compare only the primary sorting field (e.g., `v`),
18
+ * ignoring the unique identifier field (e.g., `k`).
19
+ *
20
+ * This enables efficient range queries like `primaryEqual` that find all entries
21
+ * with the same primary value regardless of their unique identifiers.
22
+ *
23
+ * @param a Value a.
24
+ * @param b Value b.
25
+ * @returns Negative if a < b, 0 if equal, positive if a > b (based on primary field only).
26
+ */
27
+ primaryAsc(a, b) {
28
+ return this.asc(a, b);
29
+ }
30
+ isPrimarySame(value, than) {
31
+ return this.primaryAsc(value, than) === 0;
32
+ }
33
+ isPrimaryLower(value, than) {
34
+ return this.primaryAsc(value, than) < 0;
35
+ }
36
+ isPrimaryHigher(value, than) {
37
+ return this.primaryAsc(value, than) > 0;
38
+ }
12
39
  };
13
40
  var NumericComparator = class extends ValueComparator {
14
41
  asc(a, b) {
@@ -422,6 +449,7 @@ var BPTree = class {
422
449
  lt: (nv, v) => this.comparator.isLower(nv, v),
423
450
  lte: (nv, v) => this.comparator.isLower(nv, v) || this.comparator.isSame(nv, v),
424
451
  equal: (nv, v) => this.comparator.isSame(nv, v),
452
+ primaryEqual: (nv, v) => this.comparator.isPrimarySame(nv, v),
425
453
  notEqual: (nv, v) => this.comparator.isSame(nv, v) === false,
426
454
  or: (nv, v) => this.ensureValues(v).some((v2) => this.comparator.isSame(nv, v2)),
427
455
  like: (nv, v) => {
@@ -438,6 +466,7 @@ var BPTree = class {
438
466
  lt: (v) => this.insertableNode(v),
439
467
  lte: (v) => this.insertableNode(v),
440
468
  equal: (v) => this.insertableNode(v),
469
+ primaryEqual: (v) => this.insertableNodeByPrimary(v),
441
470
  notEqual: (v) => this.leftestNode(),
442
471
  or: (v) => this.insertableNode(this.lowestValue(this.ensureValues(v))),
443
472
  like: (v) => this.leftestNode()
@@ -448,6 +477,7 @@ var BPTree = class {
448
477
  lt: (v) => null,
449
478
  lte: (v) => null,
450
479
  equal: (v) => this.insertableEndNode(v, this.verifierDirection.equal),
480
+ primaryEqual: (v) => null,
451
481
  notEqual: (v) => null,
452
482
  or: (v) => this.insertableEndNode(
453
483
  this.highestValue(this.ensureValues(v)),
@@ -461,10 +491,27 @@ var BPTree = class {
461
491
  lt: -1,
462
492
  lte: -1,
463
493
  equal: 1,
494
+ primaryEqual: 1,
464
495
  notEqual: 1,
465
496
  or: 1,
466
497
  like: 1
467
498
  };
499
+ /**
500
+ * Determines whether early termination is allowed for each condition.
501
+ * When true, the search will stop once a match is found and then a non-match is encountered.
502
+ * Only applicable for conditions that guarantee contiguous matches in a sorted B+Tree.
503
+ */
504
+ verifierEarlyTerminate = {
505
+ gt: false,
506
+ gte: false,
507
+ lt: false,
508
+ lte: false,
509
+ equal: true,
510
+ primaryEqual: true,
511
+ notEqual: false,
512
+ or: false,
513
+ like: false
514
+ };
468
515
  constructor(strategy, comparator, option) {
469
516
  this.strategy = strategy;
470
517
  this.comparator = comparator;
@@ -577,10 +624,11 @@ var BPTreeSync = class extends BPTree {
577
624
  capacity: this.option.capacity ?? 1e3
578
625
  });
579
626
  }
580
- getPairsRightToLeft(value, startNode, endNode, comparator) {
627
+ getPairsRightToLeft(value, startNode, endNode, comparator, earlyTerminate) {
581
628
  const pairs = [];
582
629
  let node = startNode;
583
630
  let done = false;
631
+ let hasMatched = false;
584
632
  while (!done) {
585
633
  if (endNode && node.id === endNode.id) {
586
634
  done = true;
@@ -591,12 +639,17 @@ var BPTreeSync = class extends BPTree {
591
639
  const nValue = node.values[i];
592
640
  const keys = node.keys[i];
593
641
  if (comparator(nValue, value)) {
642
+ hasMatched = true;
594
643
  let j = keys.length;
595
644
  while (j--) {
596
645
  pairs.push([keys[j], nValue]);
597
646
  }
647
+ } else if (earlyTerminate && hasMatched) {
648
+ done = true;
649
+ break;
598
650
  }
599
651
  }
652
+ if (done) break;
600
653
  if (!node.prev) {
601
654
  done = true;
602
655
  break;
@@ -605,10 +658,11 @@ var BPTreeSync = class extends BPTree {
605
658
  }
606
659
  return new Map(pairs.reverse());
607
660
  }
608
- getPairsLeftToRight(value, startNode, endNode, comparator) {
661
+ getPairsLeftToRight(value, startNode, endNode, comparator, earlyTerminate) {
609
662
  const pairs = [];
610
663
  let node = startNode;
611
664
  let done = false;
665
+ let hasMatched = false;
612
666
  while (!done) {
613
667
  if (endNode && node.id === endNode.id) {
614
668
  done = true;
@@ -618,12 +672,17 @@ var BPTreeSync = class extends BPTree {
618
672
  const nValue = node.values[i];
619
673
  const keys = node.keys[i];
620
674
  if (comparator(nValue, value)) {
675
+ hasMatched = true;
621
676
  for (let j = 0, len2 = keys.length; j < len2; j++) {
622
677
  const key = keys[j];
623
678
  pairs.push([key, nValue]);
624
679
  }
680
+ } else if (earlyTerminate && hasMatched) {
681
+ done = true;
682
+ break;
625
683
  }
626
684
  }
685
+ if (done) break;
627
686
  if (!node.next) {
628
687
  done = true;
629
688
  break;
@@ -632,12 +691,12 @@ var BPTreeSync = class extends BPTree {
632
691
  }
633
692
  return new Map(pairs);
634
693
  }
635
- getPairs(value, startNode, endNode, comparator, direction) {
694
+ getPairs(value, startNode, endNode, comparator, direction, earlyTerminate) {
636
695
  switch (direction) {
637
696
  case -1:
638
- return this.getPairsRightToLeft(value, startNode, endNode, comparator);
697
+ return this.getPairsRightToLeft(value, startNode, endNode, comparator, earlyTerminate);
639
698
  case 1:
640
- return this.getPairsLeftToRight(value, startNode, endNode, comparator);
699
+ return this.getPairsLeftToRight(value, startNode, endNode, comparator, earlyTerminate);
641
700
  default:
642
701
  throw new Error(`Direction must be -1 or 1. but got a ${direction}`);
643
702
  }
@@ -957,6 +1016,9 @@ var BPTreeSync = class extends BPTree {
957
1016
  }
958
1017
  }
959
1018
  getNode(id) {
1019
+ if (this._nodeUpdateBuffer.has(id)) {
1020
+ return this._nodeUpdateBuffer.get(id);
1021
+ }
960
1022
  if (this._nodeCreateBuffer.has(id)) {
961
1023
  return this._nodeCreateBuffer.get(id);
962
1024
  }
@@ -964,7 +1026,7 @@ var BPTreeSync = class extends BPTree {
964
1026
  return cache.raw;
965
1027
  }
966
1028
  insertableNode(value) {
967
- let node = this.root;
1029
+ let node = this.getNode(this.root.id);
968
1030
  while (!node.leaf) {
969
1031
  for (let i = 0, len = node.values.length; i < len; i++) {
970
1032
  const nValue = node.values[i];
@@ -983,6 +1045,30 @@ var BPTreeSync = class extends BPTree {
983
1045
  }
984
1046
  return node;
985
1047
  }
1048
+ /**
1049
+ * Find the insertable node using primaryAsc comparison.
1050
+ * This allows finding nodes by primary value only, ignoring unique identifiers.
1051
+ */
1052
+ insertableNodeByPrimary(value) {
1053
+ let node = this.getNode(this.root.id);
1054
+ while (!node.leaf) {
1055
+ for (let i = 0, len = node.values.length; i < len; i++) {
1056
+ const nValue = node.values[i];
1057
+ const k = node.keys;
1058
+ if (this.comparator.isPrimarySame(value, nValue)) {
1059
+ node = this.getNode(k[i]);
1060
+ break;
1061
+ } else if (this.comparator.isPrimaryLower(value, nValue)) {
1062
+ node = this.getNode(k[i]);
1063
+ break;
1064
+ } else if (i + 1 === node.values.length) {
1065
+ node = this.getNode(k[i + 1]);
1066
+ break;
1067
+ }
1068
+ }
1069
+ }
1070
+ return node;
1071
+ }
986
1072
  insertableEndNode(value, direction) {
987
1073
  const insertableNode = this.insertableNode(value);
988
1074
  let key;
@@ -1051,7 +1137,8 @@ var BPTreeSync = class extends BPTree {
1051
1137
  const endNode = this.verifierEndNode[key](value);
1052
1138
  const direction = this.verifierDirection[key];
1053
1139
  const comparator = this.verifierMap[key];
1054
- const pairs = this.getPairs(value, startNode, endNode, comparator, direction);
1140
+ const earlyTerminate = this.verifierEarlyTerminate[key];
1141
+ const pairs = this.getPairs(value, startNode, endNode, comparator, direction, earlyTerminate);
1055
1142
  if (!filterValues) {
1056
1143
  filterValues = new Set(pairs.keys());
1057
1144
  } else {
@@ -1076,7 +1163,8 @@ var BPTreeSync = class extends BPTree {
1076
1163
  const endNode = this.verifierEndNode[key](value);
1077
1164
  const direction = this.verifierDirection[key];
1078
1165
  const comparator = this.verifierMap[key];
1079
- const pairs = this.getPairs(value, startNode, endNode, comparator, direction);
1166
+ const earlyTerminate = this.verifierEarlyTerminate[key];
1167
+ const pairs = this.getPairs(value, startNode, endNode, comparator, direction, earlyTerminate);
1080
1168
  if (result === null) {
1081
1169
  result = pairs;
1082
1170
  } else {
@@ -1464,10 +1552,11 @@ var BPTreeAsync = class extends BPTree {
1464
1552
  this.lock.writeUnlock(lockId);
1465
1553
  });
1466
1554
  }
1467
- async getPairsRightToLeft(value, startNode, endNode, comparator) {
1555
+ async getPairsRightToLeft(value, startNode, endNode, comparator, earlyTerminate) {
1468
1556
  const pairs = [];
1469
1557
  let node = startNode;
1470
1558
  let done = false;
1559
+ let hasMatched = false;
1471
1560
  while (!done) {
1472
1561
  if (endNode && node.id === endNode.id) {
1473
1562
  done = true;
@@ -1478,12 +1567,17 @@ var BPTreeAsync = class extends BPTree {
1478
1567
  const nValue = node.values[i];
1479
1568
  const keys = node.keys[i];
1480
1569
  if (comparator(nValue, value)) {
1570
+ hasMatched = true;
1481
1571
  let j = keys.length;
1482
1572
  while (j--) {
1483
1573
  pairs.push([keys[j], nValue]);
1484
1574
  }
1575
+ } else if (earlyTerminate && hasMatched) {
1576
+ done = true;
1577
+ break;
1485
1578
  }
1486
1579
  }
1580
+ if (done) break;
1487
1581
  if (!node.prev) {
1488
1582
  done = true;
1489
1583
  break;
@@ -1492,10 +1586,11 @@ var BPTreeAsync = class extends BPTree {
1492
1586
  }
1493
1587
  return new Map(pairs.reverse());
1494
1588
  }
1495
- async getPairsLeftToRight(value, startNode, endNode, comparator) {
1589
+ async getPairsLeftToRight(value, startNode, endNode, comparator, earlyTerminate) {
1496
1590
  const pairs = [];
1497
1591
  let node = startNode;
1498
1592
  let done = false;
1593
+ let hasMatched = false;
1499
1594
  while (!done) {
1500
1595
  if (endNode && node.id === endNode.id) {
1501
1596
  done = true;
@@ -1505,12 +1600,17 @@ var BPTreeAsync = class extends BPTree {
1505
1600
  const nValue = node.values[i];
1506
1601
  const keys = node.keys[i];
1507
1602
  if (comparator(nValue, value)) {
1603
+ hasMatched = true;
1508
1604
  for (let j = 0, len2 = keys.length; j < len2; j++) {
1509
1605
  const key = keys[j];
1510
1606
  pairs.push([key, nValue]);
1511
1607
  }
1608
+ } else if (earlyTerminate && hasMatched) {
1609
+ done = true;
1610
+ break;
1512
1611
  }
1513
1612
  }
1613
+ if (done) break;
1514
1614
  if (!node.next) {
1515
1615
  done = true;
1516
1616
  break;
@@ -1519,12 +1619,12 @@ var BPTreeAsync = class extends BPTree {
1519
1619
  }
1520
1620
  return new Map(pairs);
1521
1621
  }
1522
- async getPairs(value, startNode, endNode, comparator, direction) {
1622
+ async getPairs(value, startNode, endNode, comparator, direction, earlyTerminate) {
1523
1623
  switch (direction) {
1524
1624
  case -1:
1525
- return await this.getPairsRightToLeft(value, startNode, endNode, comparator);
1625
+ return await this.getPairsRightToLeft(value, startNode, endNode, comparator, earlyTerminate);
1526
1626
  case 1:
1527
- return await this.getPairsLeftToRight(value, startNode, endNode, comparator);
1627
+ return await this.getPairsLeftToRight(value, startNode, endNode, comparator, earlyTerminate);
1528
1628
  default:
1529
1629
  throw new Error(`Direction must be -1 or 1. but got a ${direction}`);
1530
1630
  }
@@ -1844,6 +1944,9 @@ var BPTreeAsync = class extends BPTree {
1844
1944
  }
1845
1945
  }
1846
1946
  async getNode(id) {
1947
+ if (this._nodeUpdateBuffer.has(id)) {
1948
+ return this._nodeUpdateBuffer.get(id);
1949
+ }
1847
1950
  if (this._nodeCreateBuffer.has(id)) {
1848
1951
  return this._nodeCreateBuffer.get(id);
1849
1952
  }
@@ -1851,7 +1954,7 @@ var BPTreeAsync = class extends BPTree {
1851
1954
  return cache.raw;
1852
1955
  }
1853
1956
  async insertableNode(value) {
1854
- let node = this.root;
1957
+ let node = await this.getNode(this.root.id);
1855
1958
  while (!node.leaf) {
1856
1959
  for (let i = 0, len = node.values.length; i < len; i++) {
1857
1960
  const nValue = node.values[i];
@@ -1870,6 +1973,30 @@ var BPTreeAsync = class extends BPTree {
1870
1973
  }
1871
1974
  return node;
1872
1975
  }
1976
+ /**
1977
+ * Find the insertable node using primaryAsc comparison.
1978
+ * This allows finding nodes by primary value only, ignoring unique identifiers.
1979
+ */
1980
+ async insertableNodeByPrimary(value) {
1981
+ let node = await this.getNode(this.root.id);
1982
+ while (!node.leaf) {
1983
+ for (let i = 0, len = node.values.length; i < len; i++) {
1984
+ const nValue = node.values[i];
1985
+ const k = node.keys;
1986
+ if (this.comparator.isPrimarySame(value, nValue)) {
1987
+ node = await this.getNode(k[i]);
1988
+ break;
1989
+ } else if (this.comparator.isPrimaryLower(value, nValue)) {
1990
+ node = await this.getNode(k[i]);
1991
+ break;
1992
+ } else if (i + 1 === node.values.length) {
1993
+ node = await this.getNode(k[i + 1]);
1994
+ break;
1995
+ }
1996
+ }
1997
+ }
1998
+ return node;
1999
+ }
1873
2000
  async insertableEndNode(value, direction) {
1874
2001
  const insertableNode = await this.insertableNode(value);
1875
2002
  let key;
@@ -1939,7 +2066,8 @@ var BPTreeAsync = class extends BPTree {
1939
2066
  const endNode = await this.verifierEndNode[key](value);
1940
2067
  const direction = this.verifierDirection[key];
1941
2068
  const comparator = this.verifierMap[key];
1942
- const pairs = await this.getPairs(value, startNode, endNode, comparator, direction);
2069
+ const earlyTerminate = this.verifierEarlyTerminate[key];
2070
+ const pairs = await this.getPairs(value, startNode, endNode, comparator, direction, earlyTerminate);
1943
2071
  if (!filterValues) {
1944
2072
  filterValues = new Set(pairs.keys());
1945
2073
  } else {
@@ -1966,7 +2094,8 @@ var BPTreeAsync = class extends BPTree {
1966
2094
  const endNode = await this.verifierEndNode[key](value);
1967
2095
  const direction = this.verifierDirection[key];
1968
2096
  const comparator = this.verifierMap[key];
1969
- const pairs = await this.getPairs(value, startNode, endNode, comparator, direction);
2097
+ const earlyTerminate = this.verifierEarlyTerminate[key];
2098
+ const pairs = await this.getPairs(value, startNode, endNode, comparator, direction, earlyTerminate);
1970
2099
  if (result === null) {
1971
2100
  result = pairs;
1972
2101
  } else {
@@ -10,9 +10,9 @@ export declare class BPTreeAsync<K, V> extends BPTree<K, V> {
10
10
  private _createCachedNode;
11
11
  protected readLock<T>(callback: () => Promise<T>): Promise<T>;
12
12
  protected writeLock<T>(callback: () => Promise<T>): Promise<T>;
13
- protected getPairsRightToLeft(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean): Promise<BPTreePair<K, V>>;
14
- protected getPairsLeftToRight(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean): Promise<BPTreePair<K, V>>;
15
- protected getPairs(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean, direction: 1 | -1): Promise<BPTreePair<K, V>>;
13
+ protected getPairsRightToLeft(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean, earlyTerminate: boolean): Promise<BPTreePair<K, V>>;
14
+ protected getPairsLeftToRight(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean, earlyTerminate: boolean): Promise<BPTreePair<K, V>>;
15
+ protected getPairs(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean, direction: 1 | -1, earlyTerminate: boolean): Promise<BPTreePair<K, V>>;
16
16
  protected _createNodeId(isLeaf: boolean): Promise<string>;
17
17
  protected _createNode(isLeaf: boolean, keys: string[] | K[][], values: V[], leaf?: boolean, parent?: string | null, next?: string | null, prev?: string | null): Promise<BPTreeUnknownNode<K, V>>;
18
18
  protected _deleteEntry(node: BPTreeUnknownNode<K, V>, key: BPTreeNodeKey<K>, value: V): Promise<void>;
@@ -20,6 +20,11 @@ export declare class BPTreeAsync<K, V> extends BPTree<K, V> {
20
20
  init(): Promise<void>;
21
21
  protected getNode(id: string): Promise<BPTreeUnknownNode<K, V>>;
22
22
  protected insertableNode(value: V): Promise<BPTreeLeafNode<K, V>>;
23
+ /**
24
+ * Find the insertable node using primaryAsc comparison.
25
+ * This allows finding nodes by primary value only, ignoring unique identifiers.
26
+ */
27
+ protected insertableNodeByPrimary(value: V): Promise<BPTreeLeafNode<K, V>>;
23
28
  protected insertableEndNode(value: V, direction: 1 | -1): Promise<BPTreeLeafNode<K, V> | null>;
24
29
  protected leftestNode(): Promise<BPTreeLeafNode<K, V>>;
25
30
  protected rightestNode(): Promise<BPTreeLeafNode<K, V>>;
@@ -7,9 +7,9 @@ export declare class BPTreeSync<K, V> extends BPTree<K, V> {
7
7
  protected readonly nodes: ReturnType<typeof this._createCachedNode>;
8
8
  constructor(strategy: SerializeStrategySync<K, V>, comparator: ValueComparator<V>, option?: BPTreeConstructorOption);
9
9
  private _createCachedNode;
10
- protected getPairsRightToLeft(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean): BPTreePair<K, V>;
11
- protected getPairsLeftToRight(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean): BPTreePair<K, V>;
12
- protected getPairs(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean, direction: 1 | -1): BPTreePair<K, V>;
10
+ protected getPairsRightToLeft(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean, earlyTerminate: boolean): BPTreePair<K, V>;
11
+ protected getPairsLeftToRight(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean, earlyTerminate: boolean): BPTreePair<K, V>;
12
+ protected getPairs(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean, direction: 1 | -1, earlyTerminate: boolean): BPTreePair<K, V>;
13
13
  protected _createNodeId(isLeaf: boolean): string;
14
14
  protected _createNode(isLeaf: boolean, keys: string[] | K[][], values: V[], leaf?: boolean, parent?: string | null, next?: string | null, prev?: string | null): BPTreeUnknownNode<K, V>;
15
15
  protected _deleteEntry(node: BPTreeUnknownNode<K, V>, key: BPTreeNodeKey<K>, value: V): void;
@@ -17,6 +17,11 @@ export declare class BPTreeSync<K, V> extends BPTree<K, V> {
17
17
  init(): void;
18
18
  protected getNode(id: string): BPTreeUnknownNode<K, V>;
19
19
  protected insertableNode(value: V): BPTreeLeafNode<K, V>;
20
+ /**
21
+ * Find the insertable node using primaryAsc comparison.
22
+ * This allows finding nodes by primary value only, ignoring unique identifiers.
23
+ */
24
+ protected insertableNodeByPrimary(value: V): BPTreeLeafNode<K, V>;
20
25
  protected insertableEndNode(value: V, direction: 1 | -1): BPTreeLeafNode<K, V> | null;
21
26
  protected leftestNode(): BPTreeLeafNode<K, V>;
22
27
  protected rightestNode(): BPTreeLeafNode<K, V>;
@@ -22,6 +22,12 @@ export type BPTreeCondition<V> = Partial<{
22
22
  or: Partial<V>[];
23
23
  /** Searches for values matching the given pattern. '%' matches zero or more characters, and '_' matches exactly one character. */
24
24
  like: Partial<V>;
25
+ /**
26
+ * Searches for pairs where the primary field equals the given value.
27
+ * Uses `primaryAsc` method for comparison, which compares only the primary sorting field.
28
+ * Useful for composite values where you want to find all entries with the same primary value.
29
+ */
30
+ primaryEqual: Partial<V>;
25
31
  }>;
26
32
  export type BPTreePair<K, V> = Map<K, V>;
27
33
  export type BPTreeUnknownNode<K, V> = BPTreeInternalNode<K, V> | BPTreeLeafNode<K, V>;
@@ -66,17 +72,24 @@ export declare abstract class BPTree<K, V> {
66
72
  protected readonly verifierStartNode: Record<keyof BPTreeCondition<V>, (value: V) => Deferred<BPTreeLeafNode<K, V>>>;
67
73
  protected readonly verifierEndNode: Record<keyof BPTreeCondition<V>, (value: V) => Deferred<BPTreeLeafNode<K, V> | null>>;
68
74
  protected readonly verifierDirection: Record<keyof BPTreeCondition<V>, -1 | 1>;
75
+ /**
76
+ * Determines whether early termination is allowed for each condition.
77
+ * When true, the search will stop once a match is found and then a non-match is encountered.
78
+ * Only applicable for conditions that guarantee contiguous matches in a sorted B+Tree.
79
+ */
80
+ protected readonly verifierEarlyTerminate: Record<keyof BPTreeCondition<V>, boolean>;
69
81
  protected constructor(strategy: SerializeStrategy<K, V>, comparator: ValueComparator<V>, option?: BPTreeConstructorOption);
70
82
  private _createCachedRegexp;
71
- protected abstract getPairsRightToLeft(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean): Deferred<BPTreePair<K, V>>;
72
- protected abstract getPairsLeftToRight(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean): Deferred<BPTreePair<K, V>>;
73
- protected abstract getPairs(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean, direction: -1 | 1): Deferred<BPTreePair<K, V>>;
83
+ protected abstract getPairsRightToLeft(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean, earlyTerminate: boolean): Deferred<BPTreePair<K, V>>;
84
+ protected abstract getPairsLeftToRight(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean, earlyTerminate: boolean): Deferred<BPTreePair<K, V>>;
85
+ protected abstract getPairs(value: V, startNode: BPTreeLeafNode<K, V>, endNode: BPTreeLeafNode<K, V> | null, comparator: (nodeValue: V, value: V) => boolean, direction: -1 | 1, earlyTerminate: boolean): Deferred<BPTreePair<K, V>>;
74
86
  protected abstract _createNodeId(isLeaf: boolean): Deferred<string>;
75
87
  protected abstract _createNode(isLeaf: boolean, keys: string[] | K[][], values: V[], leaf?: boolean, parent?: string | null, next?: string | null, prev?: string | null): Deferred<BPTreeUnknownNode<K, V>>;
76
88
  protected abstract _deleteEntry(node: BPTreeUnknownNode<K, V>, key: BPTreeNodeKey<K>, value: V): Deferred<void>;
77
89
  protected abstract _insertInParent(node: BPTreeUnknownNode<K, V>, value: V, pointer: BPTreeUnknownNode<K, V>): Deferred<void>;
78
90
  protected abstract getNode(id: string): Deferred<BPTreeUnknownNode<K, V>>;
79
91
  protected abstract insertableNode(value: V): Deferred<BPTreeLeafNode<K, V>>;
92
+ protected abstract insertableNodeByPrimary(value: V): Deferred<BPTreeLeafNode<K, V>>;
80
93
  protected abstract insertableEndNode(value: V, direction: 1 | -1): Deferred<BPTreeLeafNode<K, V> | null>;
81
94
  protected abstract leftestNode(): Deferred<BPTreeLeafNode<K, V>>;
82
95
  protected abstract rightestNode(): Deferred<BPTreeLeafNode<K, V>>;
@@ -45,6 +45,25 @@ export declare abstract class ValueComparator<V> {
45
45
  isLower(value: V, than: V): boolean;
46
46
  isSame(value: V, than: V): boolean;
47
47
  isHigher(value: V, than: V): boolean;
48
+ /**
49
+ * This method is used for range queries with composite values.
50
+ * By default, it calls the `asc` method, so existing code works without changes.
51
+ *
52
+ * When using composite values (e.g., `{ k: number, v: number }`),
53
+ * override this method to compare only the primary sorting field (e.g., `v`),
54
+ * ignoring the unique identifier field (e.g., `k`).
55
+ *
56
+ * This enables efficient range queries like `primaryEqual` that find all entries
57
+ * with the same primary value regardless of their unique identifiers.
58
+ *
59
+ * @param a Value a.
60
+ * @param b Value b.
61
+ * @returns Negative if a < b, 0 if equal, positive if a > b (based on primary field only).
62
+ */
63
+ primaryAsc(a: V, b: V): number;
64
+ isPrimarySame(value: V, than: V): boolean;
65
+ isPrimaryLower(value: V, than: V): boolean;
66
+ isPrimaryHigher(value: V, than: V): boolean;
48
67
  }
49
68
  export declare class NumericComparator extends ValueComparator<number> {
50
69
  asc(a: number, b: number): number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serializable-bptree",
3
- "version": "6.0.1",
3
+ "version": "6.1.0",
4
4
  "description": "Store the B+tree flexibly, not only in-memory.",
5
5
  "types": "./dist/types/index.d.ts",
6
6
  "main": "./dist/cjs/index.cjs",