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 +15 -374
- package/dist/cjs/index.cjs +145 -16
- package/dist/esm/index.mjs +145 -16
- package/dist/types/BPTreeAsync.d.ts +8 -3
- package/dist/types/BPTreeSync.d.ts +8 -3
- package/dist/types/base/BPTree.d.ts +16 -3
- package/dist/types/base/ValueComparator.d.ts +19 -0
- package/package.json +1 -1
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
|
-
>
|
|
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
|
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
package/dist/esm/index.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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;
|