serializable-bptree 6.0.2 → 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
  }
@@ -1020,6 +1079,30 @@ var BPTreeSync = class extends BPTree {
1020
1079
  }
1021
1080
  return node;
1022
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
+ }
1023
1106
  insertableEndNode(value, direction) {
1024
1107
  const insertableNode = this.insertableNode(value);
1025
1108
  let key;
@@ -1088,7 +1171,8 @@ var BPTreeSync = class extends BPTree {
1088
1171
  const endNode = this.verifierEndNode[key](value);
1089
1172
  const direction = this.verifierDirection[key];
1090
1173
  const comparator = this.verifierMap[key];
1091
- 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);
1092
1176
  if (!filterValues) {
1093
1177
  filterValues = new Set(pairs.keys());
1094
1178
  } else {
@@ -1113,7 +1197,8 @@ var BPTreeSync = class extends BPTree {
1113
1197
  const endNode = this.verifierEndNode[key](value);
1114
1198
  const direction = this.verifierDirection[key];
1115
1199
  const comparator = this.verifierMap[key];
1116
- 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);
1117
1202
  if (result === null) {
1118
1203
  result = pairs;
1119
1204
  } else {
@@ -1501,10 +1586,11 @@ var BPTreeAsync = class extends BPTree {
1501
1586
  this.lock.writeUnlock(lockId);
1502
1587
  });
1503
1588
  }
1504
- async getPairsRightToLeft(value, startNode, endNode, comparator) {
1589
+ async getPairsRightToLeft(value, startNode, endNode, comparator, earlyTerminate) {
1505
1590
  const pairs = [];
1506
1591
  let node = startNode;
1507
1592
  let done = false;
1593
+ let hasMatched = false;
1508
1594
  while (!done) {
1509
1595
  if (endNode && node.id === endNode.id) {
1510
1596
  done = true;
@@ -1515,12 +1601,17 @@ var BPTreeAsync = class extends BPTree {
1515
1601
  const nValue = node.values[i];
1516
1602
  const keys = node.keys[i];
1517
1603
  if (comparator(nValue, value)) {
1604
+ hasMatched = true;
1518
1605
  let j = keys.length;
1519
1606
  while (j--) {
1520
1607
  pairs.push([keys[j], nValue]);
1521
1608
  }
1609
+ } else if (earlyTerminate && hasMatched) {
1610
+ done = true;
1611
+ break;
1522
1612
  }
1523
1613
  }
1614
+ if (done) break;
1524
1615
  if (!node.prev) {
1525
1616
  done = true;
1526
1617
  break;
@@ -1529,10 +1620,11 @@ var BPTreeAsync = class extends BPTree {
1529
1620
  }
1530
1621
  return new Map(pairs.reverse());
1531
1622
  }
1532
- async getPairsLeftToRight(value, startNode, endNode, comparator) {
1623
+ async getPairsLeftToRight(value, startNode, endNode, comparator, earlyTerminate) {
1533
1624
  const pairs = [];
1534
1625
  let node = startNode;
1535
1626
  let done = false;
1627
+ let hasMatched = false;
1536
1628
  while (!done) {
1537
1629
  if (endNode && node.id === endNode.id) {
1538
1630
  done = true;
@@ -1542,12 +1634,17 @@ var BPTreeAsync = class extends BPTree {
1542
1634
  const nValue = node.values[i];
1543
1635
  const keys = node.keys[i];
1544
1636
  if (comparator(nValue, value)) {
1637
+ hasMatched = true;
1545
1638
  for (let j = 0, len2 = keys.length; j < len2; j++) {
1546
1639
  const key = keys[j];
1547
1640
  pairs.push([key, nValue]);
1548
1641
  }
1642
+ } else if (earlyTerminate && hasMatched) {
1643
+ done = true;
1644
+ break;
1549
1645
  }
1550
1646
  }
1647
+ if (done) break;
1551
1648
  if (!node.next) {
1552
1649
  done = true;
1553
1650
  break;
@@ -1556,12 +1653,12 @@ var BPTreeAsync = class extends BPTree {
1556
1653
  }
1557
1654
  return new Map(pairs);
1558
1655
  }
1559
- async getPairs(value, startNode, endNode, comparator, direction) {
1656
+ async getPairs(value, startNode, endNode, comparator, direction, earlyTerminate) {
1560
1657
  switch (direction) {
1561
1658
  case -1:
1562
- return await this.getPairsRightToLeft(value, startNode, endNode, comparator);
1659
+ return await this.getPairsRightToLeft(value, startNode, endNode, comparator, earlyTerminate);
1563
1660
  case 1:
1564
- return await this.getPairsLeftToRight(value, startNode, endNode, comparator);
1661
+ return await this.getPairsLeftToRight(value, startNode, endNode, comparator, earlyTerminate);
1565
1662
  default:
1566
1663
  throw new Error(`Direction must be -1 or 1. but got a ${direction}`);
1567
1664
  }
@@ -1910,6 +2007,30 @@ var BPTreeAsync = class extends BPTree {
1910
2007
  }
1911
2008
  return node;
1912
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
+ }
1913
2034
  async insertableEndNode(value, direction) {
1914
2035
  const insertableNode = await this.insertableNode(value);
1915
2036
  let key;
@@ -1979,7 +2100,8 @@ var BPTreeAsync = class extends BPTree {
1979
2100
  const endNode = await this.verifierEndNode[key](value);
1980
2101
  const direction = this.verifierDirection[key];
1981
2102
  const comparator = this.verifierMap[key];
1982
- 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);
1983
2105
  if (!filterValues) {
1984
2106
  filterValues = new Set(pairs.keys());
1985
2107
  } else {
@@ -2006,7 +2128,8 @@ var BPTreeAsync = class extends BPTree {
2006
2128
  const endNode = await this.verifierEndNode[key](value);
2007
2129
  const direction = this.verifierDirection[key];
2008
2130
  const comparator = this.verifierMap[key];
2009
- 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);
2010
2133
  if (result === null) {
2011
2134
  result = pairs;
2012
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
  }
@@ -986,6 +1045,30 @@ var BPTreeSync = class extends BPTree {
986
1045
  }
987
1046
  return node;
988
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
+ }
989
1072
  insertableEndNode(value, direction) {
990
1073
  const insertableNode = this.insertableNode(value);
991
1074
  let key;
@@ -1054,7 +1137,8 @@ var BPTreeSync = class extends BPTree {
1054
1137
  const endNode = this.verifierEndNode[key](value);
1055
1138
  const direction = this.verifierDirection[key];
1056
1139
  const comparator = this.verifierMap[key];
1057
- 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);
1058
1142
  if (!filterValues) {
1059
1143
  filterValues = new Set(pairs.keys());
1060
1144
  } else {
@@ -1079,7 +1163,8 @@ var BPTreeSync = class extends BPTree {
1079
1163
  const endNode = this.verifierEndNode[key](value);
1080
1164
  const direction = this.verifierDirection[key];
1081
1165
  const comparator = this.verifierMap[key];
1082
- 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);
1083
1168
  if (result === null) {
1084
1169
  result = pairs;
1085
1170
  } else {
@@ -1467,10 +1552,11 @@ var BPTreeAsync = class extends BPTree {
1467
1552
  this.lock.writeUnlock(lockId);
1468
1553
  });
1469
1554
  }
1470
- async getPairsRightToLeft(value, startNode, endNode, comparator) {
1555
+ async getPairsRightToLeft(value, startNode, endNode, comparator, earlyTerminate) {
1471
1556
  const pairs = [];
1472
1557
  let node = startNode;
1473
1558
  let done = false;
1559
+ let hasMatched = false;
1474
1560
  while (!done) {
1475
1561
  if (endNode && node.id === endNode.id) {
1476
1562
  done = true;
@@ -1481,12 +1567,17 @@ var BPTreeAsync = class extends BPTree {
1481
1567
  const nValue = node.values[i];
1482
1568
  const keys = node.keys[i];
1483
1569
  if (comparator(nValue, value)) {
1570
+ hasMatched = true;
1484
1571
  let j = keys.length;
1485
1572
  while (j--) {
1486
1573
  pairs.push([keys[j], nValue]);
1487
1574
  }
1575
+ } else if (earlyTerminate && hasMatched) {
1576
+ done = true;
1577
+ break;
1488
1578
  }
1489
1579
  }
1580
+ if (done) break;
1490
1581
  if (!node.prev) {
1491
1582
  done = true;
1492
1583
  break;
@@ -1495,10 +1586,11 @@ var BPTreeAsync = class extends BPTree {
1495
1586
  }
1496
1587
  return new Map(pairs.reverse());
1497
1588
  }
1498
- async getPairsLeftToRight(value, startNode, endNode, comparator) {
1589
+ async getPairsLeftToRight(value, startNode, endNode, comparator, earlyTerminate) {
1499
1590
  const pairs = [];
1500
1591
  let node = startNode;
1501
1592
  let done = false;
1593
+ let hasMatched = false;
1502
1594
  while (!done) {
1503
1595
  if (endNode && node.id === endNode.id) {
1504
1596
  done = true;
@@ -1508,12 +1600,17 @@ var BPTreeAsync = class extends BPTree {
1508
1600
  const nValue = node.values[i];
1509
1601
  const keys = node.keys[i];
1510
1602
  if (comparator(nValue, value)) {
1603
+ hasMatched = true;
1511
1604
  for (let j = 0, len2 = keys.length; j < len2; j++) {
1512
1605
  const key = keys[j];
1513
1606
  pairs.push([key, nValue]);
1514
1607
  }
1608
+ } else if (earlyTerminate && hasMatched) {
1609
+ done = true;
1610
+ break;
1515
1611
  }
1516
1612
  }
1613
+ if (done) break;
1517
1614
  if (!node.next) {
1518
1615
  done = true;
1519
1616
  break;
@@ -1522,12 +1619,12 @@ var BPTreeAsync = class extends BPTree {
1522
1619
  }
1523
1620
  return new Map(pairs);
1524
1621
  }
1525
- async getPairs(value, startNode, endNode, comparator, direction) {
1622
+ async getPairs(value, startNode, endNode, comparator, direction, earlyTerminate) {
1526
1623
  switch (direction) {
1527
1624
  case -1:
1528
- return await this.getPairsRightToLeft(value, startNode, endNode, comparator);
1625
+ return await this.getPairsRightToLeft(value, startNode, endNode, comparator, earlyTerminate);
1529
1626
  case 1:
1530
- return await this.getPairsLeftToRight(value, startNode, endNode, comparator);
1627
+ return await this.getPairsLeftToRight(value, startNode, endNode, comparator, earlyTerminate);
1531
1628
  default:
1532
1629
  throw new Error(`Direction must be -1 or 1. but got a ${direction}`);
1533
1630
  }
@@ -1876,6 +1973,30 @@ var BPTreeAsync = class extends BPTree {
1876
1973
  }
1877
1974
  return node;
1878
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
+ }
1879
2000
  async insertableEndNode(value, direction) {
1880
2001
  const insertableNode = await this.insertableNode(value);
1881
2002
  let key;
@@ -1945,7 +2066,8 @@ var BPTreeAsync = class extends BPTree {
1945
2066
  const endNode = await this.verifierEndNode[key](value);
1946
2067
  const direction = this.verifierDirection[key];
1947
2068
  const comparator = this.verifierMap[key];
1948
- 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);
1949
2071
  if (!filterValues) {
1950
2072
  filterValues = new Set(pairs.keys());
1951
2073
  } else {
@@ -1972,7 +2094,8 @@ var BPTreeAsync = class extends BPTree {
1972
2094
  const endNode = await this.verifierEndNode[key](value);
1973
2095
  const direction = this.verifierDirection[key];
1974
2096
  const comparator = this.verifierMap[key];
1975
- 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);
1976
2099
  if (result === null) {
1977
2100
  result = pairs;
1978
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.2",
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",