serializable-bptree 2.0.0 → 3.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 +186 -10
- package/dist/cjs/index.js +931 -390
- package/dist/esm/index.js +931 -390
- package/dist/typings/BPTreeAsync.d.ts +29 -0
- package/dist/typings/BPTreeSync.d.ts +30 -0
- package/dist/typings/SerializeStrategyAsync.d.ts +21 -0
- package/dist/typings/SerializeStrategySync.d.ts +21 -0
- package/dist/typings/base/BPTree.d.ts +138 -0
- package/dist/typings/{SerializeStrategy.d.ts → base/SerializeStrategy.d.ts} +7 -19
- package/dist/typings/base/ValueComparator.d.ts +56 -0
- package/dist/typings/index.d.ts +7 -3
- package/dist/typings/utils/CacheStorage.d.ts +3 -0
- package/package.json +5 -1
- package/dist/typings/BPTree.d.ts +0 -127
- package/dist/typings/ValueComparator.d.ts +0 -18
- package/dist/typings/utils/BinarySearch.d.ts +0 -9
package/README.md
CHANGED
|
@@ -7,9 +7,13 @@ This is a B+tree that's totally okay with duplicate values. If you need to keep
|
|
|
7
7
|
|
|
8
8
|
```typescript
|
|
9
9
|
import { readFileSync, writeFileSync, existsSync } from 'fs'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
BPTreeSync,
|
|
12
|
+
SerializeStrategySync,
|
|
13
|
+
NumericComparator
|
|
14
|
+
} from 'serializable-bptree'
|
|
11
15
|
|
|
12
|
-
class
|
|
16
|
+
class FileStoreStrategySync extends SerializeStrategySync<K, V> {
|
|
13
17
|
id(): number {
|
|
14
18
|
const random = Math.ceil(Math.random()*1000000)
|
|
15
19
|
return random
|
|
@@ -40,11 +44,12 @@ class FileStoreStrategy extends SerializeStrategy<K, V> {
|
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
const order = 5
|
|
43
|
-
const tree = new
|
|
44
|
-
new
|
|
47
|
+
const tree = new BPTreeSync(
|
|
48
|
+
new FileStoreStrategySync(order),
|
|
45
49
|
new NumericComparator()
|
|
46
50
|
)
|
|
47
51
|
|
|
52
|
+
tree.init()
|
|
48
53
|
tree.insert('a', 1)
|
|
49
54
|
tree.insert('b', 2)
|
|
50
55
|
tree.insert('c', 3)
|
|
@@ -61,6 +66,8 @@ tree.where({ gt: 0, lt: 4 }) // [{ key: 'a', value: 1 }, { key: 'c', value: 3 }]
|
|
|
61
66
|
|
|
62
67
|
Firstly, in most cases, there is no need to use a B+tree in JavaScript. This is because there is a great alternative, the Map object. Nonetheless, if you need to retrieve values in a sorted order, a B+tree can be a good solution. These cases are often related to databases, and you may want to store this state not just in memory, but on a remote server or in a file. In this case, **serializable-bptree** can help you.
|
|
63
68
|
|
|
69
|
+
Additionally, this library supports asynchronous operations. Please refer to the section below for instructions on using it asynchronously.
|
|
70
|
+
|
|
64
71
|
## How to use
|
|
65
72
|
|
|
66
73
|
### Node.js (cjs)
|
|
@@ -70,14 +77,29 @@ npm i serializable-bptree
|
|
|
70
77
|
```
|
|
71
78
|
|
|
72
79
|
```typescript
|
|
73
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
BPTreeSync,
|
|
82
|
+
BPTreeAsync,
|
|
83
|
+
SerializeStrategySync,
|
|
84
|
+
SerializeStrategyAsync,
|
|
85
|
+
NumericComparator,
|
|
86
|
+
StringComparator
|
|
87
|
+
} from 'serializable-bptree'
|
|
74
88
|
```
|
|
75
89
|
|
|
76
90
|
### Browser (esm)
|
|
77
91
|
|
|
78
92
|
```html
|
|
79
93
|
<script type="module">
|
|
80
|
-
import {
|
|
94
|
+
import {
|
|
95
|
+
BPTreeSync,
|
|
96
|
+
BPTreeAsync,
|
|
97
|
+
InMemoryStoreStrategySync,
|
|
98
|
+
InMemoryStoreStrategyAsync,
|
|
99
|
+
ValueComparator,
|
|
100
|
+
NumericComparator,
|
|
101
|
+
StringComparator
|
|
102
|
+
} from 'https://cdn.jsdelivr.net/npm/serializable-bptree@3.x.x/dist/esm/index.min.js'
|
|
81
103
|
</script>
|
|
82
104
|
```
|
|
83
105
|
|
|
@@ -107,11 +129,45 @@ class AgeComparator extends ValueComparator<MyObject> {
|
|
|
107
129
|
asc(a: MyObject, b: MyObject): number {
|
|
108
130
|
return a.age - b.age
|
|
109
131
|
}
|
|
132
|
+
|
|
133
|
+
match(value: MyObject): string {
|
|
134
|
+
return value.age
|
|
135
|
+
}
|
|
110
136
|
}
|
|
111
137
|
```
|
|
112
138
|
|
|
139
|
+
#### asc
|
|
140
|
+
|
|
113
141
|
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.
|
|
114
142
|
|
|
143
|
+
#### match
|
|
144
|
+
|
|
145
|
+
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: number }`, 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.
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
interface MyObject {
|
|
149
|
+
country: string
|
|
150
|
+
capital: string
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
class CompositeComparator extends ValueComparator<MyObject> {
|
|
154
|
+
...
|
|
155
|
+
match(value: MyObject): string {
|
|
156
|
+
return value.capital
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
For a tree with simple structure, without complex nesting, returning the value directly would be sufficient.
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
class StringComparator extends ValueComparator<string> {
|
|
165
|
+
match(value: string): string {
|
|
166
|
+
return value
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
115
171
|
### Serialize strategy
|
|
116
172
|
|
|
117
173
|
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.
|
|
@@ -119,9 +175,9 @@ A B+tree instance is made up of numerous nodes. You would want to store this val
|
|
|
119
175
|
You need to construct a logic for input/output from the file by inheriting the SerializeStrategy class. Look at the class structure below:
|
|
120
176
|
|
|
121
177
|
```typescript
|
|
122
|
-
import {
|
|
178
|
+
import { SerializeStrategySync } from 'serializable-bptree'
|
|
123
179
|
|
|
124
|
-
class
|
|
180
|
+
class MyFileIOStrategySync extends SerializeStrategySync {
|
|
125
181
|
id(): number
|
|
126
182
|
read(id: number): BPTreeNode<K, V>
|
|
127
183
|
write(id: number, node: BPTreeNode<K, V>): void
|
|
@@ -227,14 +283,134 @@ import { StringComparator } from 'serializable-bptree'
|
|
|
227
283
|
|
|
228
284
|
#### SerializeStrategy
|
|
229
285
|
|
|
230
|
-
* `
|
|
286
|
+
* `InMemoryStoreStrategySync`
|
|
287
|
+
* `InMemoryStoreStrategyAsync`
|
|
231
288
|
|
|
232
289
|
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.
|
|
233
290
|
|
|
234
291
|
```typescript
|
|
235
|
-
import {
|
|
292
|
+
import {
|
|
293
|
+
InMemoryStoreStrategySync,
|
|
294
|
+
InMemoryStoreStrategyAsync
|
|
295
|
+
} from 'serializable-bptree'
|
|
236
296
|
```
|
|
237
297
|
|
|
298
|
+
## Data Query Condition Clause
|
|
299
|
+
|
|
300
|
+
This library supports various conditional clauses. Currently, it supports **gte**, **gt**, **lte**, **lt**, **equal**, **notEqual**, and **like** conditions. Each condition is as follows:
|
|
301
|
+
|
|
302
|
+
### `gte`
|
|
303
|
+
|
|
304
|
+
Queries values that are greater than or equal to the given value.
|
|
305
|
+
|
|
306
|
+
### `gt`
|
|
307
|
+
|
|
308
|
+
Queries values that are greater than the given value.
|
|
309
|
+
|
|
310
|
+
### `lte`
|
|
311
|
+
|
|
312
|
+
Queries values that are less than or equal to the given value.
|
|
313
|
+
|
|
314
|
+
### `lt`
|
|
315
|
+
|
|
316
|
+
Queries values that are less than the given value.
|
|
317
|
+
|
|
318
|
+
### `equal`
|
|
319
|
+
|
|
320
|
+
Queries values that match the given value.
|
|
321
|
+
|
|
322
|
+
### `notEqual`
|
|
323
|
+
|
|
324
|
+
Queries values that do not match the given value.
|
|
325
|
+
|
|
326
|
+
### `like`
|
|
327
|
+
|
|
328
|
+
Queries values that contain the given value in a manner similar to regular expressions. Special characters such as % and _ can be used.
|
|
329
|
+
|
|
330
|
+
**%** 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"**.
|
|
331
|
+
|
|
332
|
+
**_** matches exactly one character.
|
|
333
|
+
Using **p_t**, it can match any string where the underscore is replaced by any character, such as "pit", "put", etc.
|
|
334
|
+
|
|
335
|
+
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.
|
|
336
|
+
|
|
337
|
+
## Using Asynchronously
|
|
338
|
+
|
|
339
|
+
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:
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import { existsSync } from 'fs'
|
|
343
|
+
import { readFile, writeFile } from 'fs/promises'
|
|
344
|
+
import {
|
|
345
|
+
BPTreeAsync,
|
|
346
|
+
SerializeStrategyAsync,
|
|
347
|
+
NumericComparator,
|
|
348
|
+
StringComparator
|
|
349
|
+
} from 'serializable-bptree'
|
|
350
|
+
|
|
351
|
+
class FileStoreStrategyAsync extends SerializeStrategyAsync<K, V> {
|
|
352
|
+
async id(): Promise<number> {
|
|
353
|
+
const random = Math.ceil(Math.random()*1000000)
|
|
354
|
+
return random
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async read(id: number): Promise<BPTreeNode<K, V>> {
|
|
358
|
+
const raw = await readFile(id.toString(), 'utf8')
|
|
359
|
+
return JSON.parse(raw)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async write(id: number, node: BPTreeNode<K, V>): Promise<void> {
|
|
363
|
+
const stringify = JSON.stringify(node)
|
|
364
|
+
await writeFile(id.toString(), stringify, 'utf8')
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async readHead(): Promise<SerializeStrategyHead|null> {
|
|
368
|
+
if (!existsSync('head')) {
|
|
369
|
+
return null
|
|
370
|
+
}
|
|
371
|
+
const raw = await readFile('head', 'utf8')
|
|
372
|
+
return JSON.parse(raw)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async writeHead(head: SerializeStrategyHead): Promise<void> {
|
|
376
|
+
const stringify = JSON.stringify(head)
|
|
377
|
+
await writeFile('head', stringify, 'utf8')
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const order = 5
|
|
382
|
+
const tree = new BPTreeAsync(
|
|
383
|
+
new FileStoreStrategyAsync(order),
|
|
384
|
+
new NumericComparator()
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
await tree.init()
|
|
388
|
+
await tree.insert('a', 1)
|
|
389
|
+
await tree.insert('b', 2)
|
|
390
|
+
await tree.insert('c', 3)
|
|
391
|
+
|
|
392
|
+
await tree.delete('b', 2)
|
|
393
|
+
|
|
394
|
+
await tree.where({ equal: 1 }) // [{ key: 'a', value: 1 }]
|
|
395
|
+
await tree.where({ gt: 1 }) // [{ key: 'c', value: 3 }]
|
|
396
|
+
await tree.where({ lt: 2 }) // [{ key: 'a', value: 1 }]
|
|
397
|
+
await tree.where({ gt: 0, lt: 4 }) // [{ key: 'a', value: 1 }, { key: 'c', value: 3 }]
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
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.
|
|
401
|
+
|
|
402
|
+
## Precautions for Use
|
|
403
|
+
|
|
404
|
+
### Synchronization Issue
|
|
405
|
+
|
|
406
|
+
The serializable-bptree minimizes file I/O by storing loaded nodes in-memory. This approach works well in situations where there is a 1:1 relationship between the remote storage and the client. However, in a 1:n scenario, where multiple clients read from and write to a single remote storage, data inconsistency between the remote storage and the clients can occur.
|
|
407
|
+
|
|
408
|
+
To solve this problem, it's necessary to update the cached nodes. The forceUpdate method was created for this purpose. It fetches the node data cached in the tree instance again. To use this feature, when you save data to the remote storage, you must send a signal to all clients connected to that remote storage indicating that the node has been updated. Clients must receive this signal and configure logic to call the **forceUpdate** method; however, this goes beyond the scope of the library, so you must implement it yourself.
|
|
409
|
+
|
|
410
|
+
### Concurrency Issue in Asynchronous Trees
|
|
411
|
+
|
|
412
|
+
This issue occurs only in asynchronous trees and can also occur in a 1:1 relationship between remote storage and client. During the process of inserting/removing data asynchronously, querying the data can result in inconsistent data. To prevent concurrency issues, do not query data while inserting/removing it.
|
|
413
|
+
|
|
238
414
|
## LICENSE
|
|
239
415
|
|
|
240
416
|
MIT LICENSE
|