@frostpillar/frostpillar-btree 0.2.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/LICENSE +21 -0
- package/README-JA.md +912 -0
- package/README.md +912 -0
- package/dist/InMemoryBTree.d.ts +45 -0
- package/dist/btree/autoScale.d.ts +9 -0
- package/dist/btree/bulkLoad.d.ts +5 -0
- package/dist/btree/deleteRange.d.ts +3 -0
- package/dist/btree/integrity-helpers.d.ts +6 -0
- package/dist/btree/integrity.d.ts +2 -0
- package/dist/btree/mutations.d.ts +12 -0
- package/dist/btree/navigation.d.ts +23 -0
- package/dist/btree/rangeQuery.d.ts +3 -0
- package/dist/btree/rebalance.d.ts +4 -0
- package/dist/btree/serialization.d.ts +20 -0
- package/dist/btree/stats.d.ts +2 -0
- package/dist/btree/types.d.ts +113 -0
- package/dist/chunk-ZA3EQNDI.js +1902 -0
- package/dist/concurrency/ConcurrentInMemoryBTree.d.ts +56 -0
- package/dist/concurrency/helpers.d.ts +27 -0
- package/dist/concurrency/index.d.ts +2 -0
- package/dist/concurrency/types.d.ts +41 -0
- package/dist/core.cjs +1919 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.js +10 -0
- package/dist/errors.d.ts +9 -0
- package/dist/frostpillar-btree-core.min.js +1 -0
- package/dist/frostpillar-btree.min.js +1 -0
- package/dist/index.cjs +2230 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +316 -0
- package/package.json +80 -0
package/README.md
ADDED
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
# frostpillar-btree
|
|
2
|
+
|
|
3
|
+
[English/英語](./README.md) | [Japanese/日本語](./README-JA.md)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@frostpillar/frostpillar-btree)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
[](https://github.com/hjmsano/frostpillar-btree/actions/workflows/ci.yml)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
|
|
10
|
+
A [B+ tree](https://en.wikipedia.org/wiki/B%2B_tree) is a self-balancing tree data structure that keeps data sorted and supports searches, insertions, and deletions in O(log n) time. Unlike a plain sorted array, it handles frequent inserts and deletes efficiently without re-sorting.
|
|
11
|
+
|
|
12
|
+
`frostpillar-btree` is a tiny, zero-dependency in-memory B+ tree for TypeScript, Node.js, and browser JavaScript. Use it as a sorted key-value store for task queues, priority lists, leaderboards, or any scenario where you need fast ordered access. It also supports coordinated state across multiple processes via a pluggable shared store.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- **Zero dependencies** -- no runtime packages required
|
|
17
|
+
- **Works everywhere** -- Node.js (ESM and CJS), TypeScript, and browsers (IIFE bundle)
|
|
18
|
+
- **Dual browser bundles** -- full API bundle and smaller single-process core bundle
|
|
19
|
+
- **Configurable key uniqueness** -- `'replace'` (default, map semantics), `'reject'` (unique constraint), or `'allow'` (multimap)
|
|
20
|
+
- **Full TypeScript type safety** -- strict generics with branded `EntryId`
|
|
21
|
+
- **Cross-process coordination** -- `ConcurrentInMemoryBTree` provides optimistic concurrency via a pluggable shared store
|
|
22
|
+
|
|
23
|
+
## Quick Example
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { InMemoryBTree } from '@frostpillar/frostpillar-btree';
|
|
27
|
+
|
|
28
|
+
const tree = new InMemoryBTree<number, string>({
|
|
29
|
+
compareKeys: (left: number, right: number): number => left - right,
|
|
30
|
+
enableEntryIdLookup: true,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const idTen = tree.put(10, 'ten');
|
|
34
|
+
tree.put(20, 'twenty');
|
|
35
|
+
|
|
36
|
+
console.log(tree.peekById(idTen));
|
|
37
|
+
console.log(tree.range(10, 20));
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Table of Contents
|
|
43
|
+
|
|
44
|
+
- [Getting Started](#getting-started)
|
|
45
|
+
- [User Manual](#user-manual)
|
|
46
|
+
- [InMemoryBTree (Single-Process)](#inmemorybtree-single-process)
|
|
47
|
+
- [ConcurrentInMemoryBTree (Multi-Process)](#concurrentinmemorybtree-multi-process)
|
|
48
|
+
- [Error Handling](#error-handling)
|
|
49
|
+
- [API Reference](#api-reference)
|
|
50
|
+
- [InMemoryBTree](#inmemorybtree)
|
|
51
|
+
- [ConcurrentInMemoryBTree](#concurrentinmemorybtree)
|
|
52
|
+
- [Exported Types](#exported-types)
|
|
53
|
+
- [How to Contribute](#how-to-contribute)
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Getting Started
|
|
58
|
+
|
|
59
|
+
### Installation (Node.js / TypeScript)
|
|
60
|
+
|
|
61
|
+
Install:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install @frostpillar/frostpillar-btree
|
|
65
|
+
# or
|
|
66
|
+
pnpm add @frostpillar/frostpillar-btree
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If you only need the single-process API, import from the core subpath:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { InMemoryBTree } from '@frostpillar/frostpillar-btree/core';
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### CommonJS
|
|
76
|
+
|
|
77
|
+
CommonJS is also supported. Use `require()` as usual:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
const { InMemoryBTree } = require('@frostpillar/frostpillar-btree');
|
|
81
|
+
// or the core subpath:
|
|
82
|
+
const { InMemoryBTree } = require('@frostpillar/frostpillar-btree/core');
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Installation (Browser)
|
|
86
|
+
|
|
87
|
+
A minified IIFE bundle is available on the [GitHub Releases](https://github.com/hjmsano/frostpillar-btree/releases) page. Both bundles target ES2020:
|
|
88
|
+
|
|
89
|
+
- `frostpillar-btree.min.js` (full API): exposes `window.FrostpillarBTree`
|
|
90
|
+
- `frostpillar-btree-core.min.js` (single-process core): exposes `window.FrostpillarBTreeCore`
|
|
91
|
+
|
|
92
|
+
1. Download the bundle you need from the Releases page.
|
|
93
|
+
2. Place it in your static assets directory.
|
|
94
|
+
3. Load with a `<script>` tag:
|
|
95
|
+
|
|
96
|
+
```html
|
|
97
|
+
<script src="./frostpillar-btree.min.js"></script>
|
|
98
|
+
<!-- or -->
|
|
99
|
+
<script src="./frostpillar-btree-core.min.js"></script>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
After loading, use the matching global:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
const { InMemoryBTree } = window.FrostpillarBTree;
|
|
106
|
+
// or:
|
|
107
|
+
// const { InMemoryBTree } = window.FrostpillarBTreeCore;
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Compatibility
|
|
111
|
+
|
|
112
|
+
| Environment | Requirement |
|
|
113
|
+
| ----------- | ----------------------------------------------------------------- |
|
|
114
|
+
| Node.js | >= 24.0.0 (ESM and CJS) |
|
|
115
|
+
| Browser | ES2020-compatible (Chrome 80+, Firefox 74+, Safari 14+, Edge 80+) |
|
|
116
|
+
| TypeScript | >= 5.0 |
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## User Manual
|
|
121
|
+
|
|
122
|
+
> **Error overview:** Operations may throw `BTreeValidationError` (bad comparator or config), `BTreeInvariantError` (corrupted tree structure), or `BTreeConcurrencyError` (concurrent retry exhaustion). See [Error Handling](#error-handling) for details and examples.
|
|
123
|
+
|
|
124
|
+
### InMemoryBTree (Single-Process)
|
|
125
|
+
|
|
126
|
+
`InMemoryBTree` is the core class for single-process use. It stores key-value pairs in a B+ tree structure with O(log n) put, remove, and lookup operations.
|
|
127
|
+
|
|
128
|
+
#### Creating a Tree
|
|
129
|
+
|
|
130
|
+
You must provide a `compareKeys` function that defines the sort order. It follows the same convention as `Array.prototype.sort`: return negative if `left < right`, positive if `left > right`, and `0` if equal.
|
|
131
|
+
|
|
132
|
+
**Node.js / TypeScript:**
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { InMemoryBTree } from '@frostpillar/frostpillar-btree';
|
|
136
|
+
|
|
137
|
+
const tree = new InMemoryBTree<number, string>({
|
|
138
|
+
compareKeys: (left: number, right: number): number => left - right,
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Browser:**
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
const { InMemoryBTree } = window.FrostpillarBTree;
|
|
146
|
+
|
|
147
|
+
const tree = new InMemoryBTree({
|
|
148
|
+
compareKeys: (left, right) => {
|
|
149
|
+
if (left < right) return -1;
|
|
150
|
+
if (left > right) return 1;
|
|
151
|
+
return 0;
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
You can optionally tune the tree shape with `maxLeafEntries` and `maxBranchChildren` (both default to 64, minimum 3, maximum 16384):
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const tree = new InMemoryBTree<string, number>({
|
|
160
|
+
compareKeys: (a, b) => a.localeCompare(b),
|
|
161
|
+
maxLeafEntries: 128,
|
|
162
|
+
maxBranchChildren: 128,
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### Inserting Entries
|
|
167
|
+
|
|
168
|
+
`put()` adds a key-value pair and returns an `EntryId` (a branded `number`). In `'replace'` mode, inserting an existing key returns the original entry's `EntryId`. You can use this ID later to peek, update, or remove the specific entry.
|
|
169
|
+
|
|
170
|
+
**Node.js / TypeScript:**
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const id1 = tree.put(10, 'ten');
|
|
174
|
+
const id2 = tree.put(20, 'twenty');
|
|
175
|
+
tree.put(10, 'updated ten'); // default 'replace' mode: overwrites, id1 is preserved
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Browser:**
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
const id1 = tree.put(10, 'ten');
|
|
182
|
+
const id2 = tree.put(20, 'twenty');
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**`putMany(entries)`** -- insert multiple pre-sorted entries at once. When the tree is empty, uses an optimized bulk-load that builds the tree in O(n) instead of O(n log n). Entries must be sorted in ascending key order (strictly ascending when `duplicateKeys` is `'reject'` or `'replace'`):
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const ids = tree.putMany([
|
|
189
|
+
{ key: 1, value: 'a' },
|
|
190
|
+
{ key: 2, value: 'b' },
|
|
191
|
+
{ key: 3, value: 'c' },
|
|
192
|
+
]);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### Reading Entries
|
|
196
|
+
|
|
197
|
+
**`peekById(entryId)`** -- look up a specific entry by its ID without removing it:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
const entry = tree.peekById(id1);
|
|
201
|
+
// { entryId: 0, key: 10, value: 'updated ten' } or null if not found
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**`peekFirst()`** -- get the smallest entry without removing it:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
const first = tree.peekFirst();
|
|
208
|
+
// { entryId: ..., key: 10, value: 'ten' } or null if empty
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**`get(key)`** -- look up the value for a key without allocating a result array:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const value = tree.get(10); // 'ten' or null if not found
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**`hasKey(key)`** -- check if at least one entry exists for a key:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
const exists = tree.hasKey(10); // true
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**`findFirst(key)`** -- find the first entry matching a key. Returns a `BTreeEntry` or `null`:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const entry = tree.findFirst(10);
|
|
227
|
+
// { entryId: ..., key: 10, value: 'ten' } or null
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**`findLast(key)`** -- find the last (most recently inserted) entry matching a key. Returns a `BTreeEntry` or `null`:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
const entry = tree.findLast(10);
|
|
234
|
+
// { entryId: ..., key: 10, value: 'ten' } or null
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**`peekLast()`** -- get the largest entry without removing it:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
const last = tree.peekLast();
|
|
241
|
+
// { entryId: ..., key: 20, value: 'twenty' } or null if empty
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### Updating Entries
|
|
245
|
+
|
|
246
|
+
**`updateById(entryId, newValue)`** -- update the value of an existing entry. The key and position in the tree remain unchanged:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
const updated = tree.updateById(id1, 'TEN');
|
|
250
|
+
// { entryId: 0, key: 10, value: 'TEN' } or null if not found
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### Removing Entries
|
|
254
|
+
|
|
255
|
+
**`remove(key)`** -- remove the first matching entry by key:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
const removed = tree.remove(10);
|
|
259
|
+
// { entryId: ..., key: 10, value: 'ten' } or null if not found
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**`removeById(entryId)`** -- remove a specific entry by its ID:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
const removed = tree.removeById(id2);
|
|
266
|
+
// { entryId: ..., key: 20, value: 'twenty' } or null if not found
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**`popFirst()`** -- remove and return the smallest entry (useful for priority queues):
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
const first = tree.popFirst();
|
|
273
|
+
// { entryId: ..., key: 10, value: 'ten' } or null if empty
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**`popLast()`** -- remove and return the largest entry:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
const last = tree.popLast();
|
|
280
|
+
// { entryId: ..., key: 20, value: 'twenty' } or null if empty
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**`clear()`** -- remove all entries and reset the tree to its empty state in O(1). The internal sequence counter is also reset, so new `EntryId` values start from zero. Any `EntryId` obtained before `clear()` becomes invalid.
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
tree.clear();
|
|
287
|
+
tree.size(); // 0
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**`deleteRange(startKey, endKey, options?)`** -- remove all entries in a range. Follows the same bound semantics as `range`:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
tree.deleteRange(2, 4); // removes keys 2, 3, 4 — returns count of deleted entries
|
|
294
|
+
tree.deleteRange(2, 4, { lowerBound: 'exclusive' }); // removes keys 3, 4
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
#### Querying
|
|
298
|
+
|
|
299
|
+
**`count(startKey, endKey, options?)`** -- count entries in a range without allocating a result array. Follows the same bound semantics as `range`:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
tree.put(1, 'a');
|
|
303
|
+
tree.put(2, 'b');
|
|
304
|
+
tree.put(3, 'c');
|
|
305
|
+
tree.put(4, 'd');
|
|
306
|
+
|
|
307
|
+
tree.count(2, 3); // 2
|
|
308
|
+
tree.count(1, 4, { lowerBound: 'exclusive' }); // 3
|
|
309
|
+
tree.count(1, 4, { upperBound: 'exclusive' }); // 3
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**`range(startKey, endKey, options?)`** -- get all entries between `startKey` and `endKey` (inclusive on both bounds by default):
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
tree.put(1, 'a');
|
|
316
|
+
tree.put(2, 'b');
|
|
317
|
+
tree.put(3, 'c');
|
|
318
|
+
tree.put(4, 'd');
|
|
319
|
+
|
|
320
|
+
const entries = tree.range(2, 3);
|
|
321
|
+
// [{ entryId: ..., key: 2, value: 'b' }, { entryId: ..., key: 3, value: 'c' }]
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
You can use `RangeBounds` to control whether each bound is inclusive or exclusive:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
tree.range(2, 4, { lowerBound: 'exclusive' });
|
|
328
|
+
// excludes key 2 → [{ key: 3, ... }, { key: 4, ... }]
|
|
329
|
+
|
|
330
|
+
tree.range(2, 4, { upperBound: 'exclusive' });
|
|
331
|
+
// excludes key 4 → [{ key: 2, ... }, { key: 3, ... }]
|
|
332
|
+
|
|
333
|
+
tree.range(2, 4, { lowerBound: 'exclusive', upperBound: 'exclusive' });
|
|
334
|
+
// excludes both → [{ key: 3, ... }]
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**`nextHigherKey(key)`** -- return the smallest key that is strictly greater than the given key:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
tree.put(10, 'a');
|
|
341
|
+
tree.put(20, 'b');
|
|
342
|
+
tree.nextHigherKey(10); // 20
|
|
343
|
+
tree.nextHigherKey(20); // null
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**`nextLowerKey(key)`** -- return the largest key that is strictly less than the given key:
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
tree.nextLowerKey(20); // 10
|
|
350
|
+
tree.nextLowerKey(10); // null
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**`getPairOrNextLower(key)`** -- return the entry matching the key, or the entry with the largest key strictly less than the given key:
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
tree.getPairOrNextLower(15); // { entryId: ..., key: 10, value: 'a' }
|
|
357
|
+
tree.getPairOrNextLower(10); // { entryId: ..., key: 10, value: 'a' } (exact match)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
#### Iterating
|
|
361
|
+
|
|
362
|
+
**`entries()`** -- lazily iterate all entries in ascending key order without allocating a snapshot array:
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
for (const entry of tree.entries()) {
|
|
366
|
+
console.log(entry.key, entry.value);
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**`entriesReversed()`** -- lazily iterate all entries in descending key order:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
for (const entry of tree.entriesReversed()) {
|
|
374
|
+
console.log(entry.key, entry.value); // largest key first
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**`keys()`** / **`values()`** -- iterate keys or values only:
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
const allKeys = [...tree.keys()]; // [1, 2, 3]
|
|
382
|
+
const allValues = [...tree.values()]; // ['a', 'b', 'c']
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**`for...of`** -- the tree itself is iterable (delegates to `entries()`):
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
for (const entry of tree) {
|
|
389
|
+
console.log(entry.key, entry.value);
|
|
390
|
+
}
|
|
391
|
+
const asArray = [...tree]; // spread also works
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**`forEach(callback, thisArg?)`** -- visit each entry in ascending key order:
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
tree.forEach((entry) => {
|
|
398
|
+
console.log(entry.key, entry.value);
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**`snapshot()`** -- get all entries in sorted order:
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
const all = tree.snapshot();
|
|
406
|
+
// [{ entryId, key, value }, ...]
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**`size()`** -- get the total number of entries:
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
const count = tree.size(); // 4
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
#### Diagnostics
|
|
416
|
+
|
|
417
|
+
**`getStats()`** -- inspect the tree's internal structure:
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
const stats = tree.getStats();
|
|
421
|
+
// { height: 1, leafCount: 1, branchCount: 0, entryCount: 4 }
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**`assertInvariants()`** -- verify B+ tree structural integrity. Throws `BTreeInvariantError` if the tree is corrupted. Useful in tests:
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
tree.assertInvariants(); // throws if invalid
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
#### Clone and Serialization
|
|
431
|
+
|
|
432
|
+
**`clone()`** -- create a structurally independent deep copy:
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
const copy = tree.clone();
|
|
436
|
+
copy.put(99, 'new');
|
|
437
|
+
tree.hasKey(99); // false — original is unaffected
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**`toJSON()` / `fromJSON()`** -- serialize and reconstruct:
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
const json = tree.toJSON();
|
|
444
|
+
const restored = InMemoryBTree.fromJSON(json, (a, b) => a - b);
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
#### Key Uniqueness Policy
|
|
448
|
+
|
|
449
|
+
Control how `put` handles duplicate keys via the `duplicateKeys` option:
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
const tree = new InMemoryBTree<number, string>({
|
|
453
|
+
compareKeys: (a, b) => a - b,
|
|
454
|
+
duplicateKeys: 'replace', // default
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
| Policy | Behavior | Use case |
|
|
459
|
+
| --------------------- | ------------------------------------------------------------------------------ | ------------------------------------- |
|
|
460
|
+
| `'replace'` (default) | Overwrites the value of the existing entry and returns its original `EntryId`. | Key-value map / dictionary |
|
|
461
|
+
| `'reject'` | Throws `BTreeValidationError` if the key already exists. | Unique index / set |
|
|
462
|
+
| `'allow'` | Allows multiple entries with the same key, ordered by insertion time. | Multimap / event log / priority queue |
|
|
463
|
+
|
|
464
|
+
#### Behavior Notes
|
|
465
|
+
|
|
466
|
+
- `range(start, end)` is inclusive on both bounds by default. Pass `RangeBounds` to use exclusive bounds. Returns `[]` when `start > end`.
|
|
467
|
+
- `EntryId` is a branded `number` starting at `0`. Since `0` is falsy in JavaScript, avoid `if (entryId)` checks — use `if (entryId !== null)` or `if (entryId !== undefined)` instead.
|
|
468
|
+
- Comparator contract checks (including finiteness, reflexivity, and transitivity) are enforced by `assertInvariants()`, not by eager checks on every normal operation.
|
|
469
|
+
- `compareKeys` must be a function at runtime. Passing a non-function value throws `BTreeValidationError`.
|
|
470
|
+
- `enableEntryIdLookup` defaults to `false`. Set `enableEntryIdLookup: true` only when you need `peekById` / `updateById` / `removeById`.
|
|
471
|
+
- `autoScale` defaults to `false`. When `true`, node capacity tiers grow with entry count (32 -> 64 -> 128 -> 256 -> 512 for leaves). autoScale only increases capacity -- it never shrinks.
|
|
472
|
+
|
|
473
|
+
| Entry Count | maxLeafEntries | maxBranchChildren |
|
|
474
|
+
| ----------- | -------------- | ----------------- |
|
|
475
|
+
| 0+ | 32 | 32 |
|
|
476
|
+
| 1,000+ | 64 | 64 |
|
|
477
|
+
| 10,000+ | 128 | 128 |
|
|
478
|
+
| 100,000+ | 256 | 128 |
|
|
479
|
+
| 1,000,000+ | 512 | 256 |
|
|
480
|
+
|
|
481
|
+
- `autoScale` cannot be combined with explicit `maxLeafEntries` or `maxBranchChildren`.
|
|
482
|
+
- `fromJSON` rejects payloads with more than `1,000,000` entries.
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
### ConcurrentInMemoryBTree (Multi-Process)
|
|
487
|
+
|
|
488
|
+
`ConcurrentInMemoryBTree` enables multiple processes or instances to share tree state through a pluggable shared store. It uses optimistic concurrency control: each mutation is appended to the store, and conflicts are resolved by re-syncing and retrying.
|
|
489
|
+
|
|
490
|
+
#### How It Works
|
|
491
|
+
|
|
492
|
+
1. Each instance holds a local `InMemoryBTree` as a cache.
|
|
493
|
+
2. Before reads, the instance syncs from the shared store.
|
|
494
|
+
3. For writes, the instance appends mutations to the store. If a concurrent write occurred, it re-syncs and retries (up to `maxRetries`, default 16).
|
|
495
|
+
4. All async operations on a single instance are serialized to prevent double-apply.
|
|
496
|
+
|
|
497
|
+
#### Implementing SharedTreeStore
|
|
498
|
+
|
|
499
|
+
`ConcurrentInMemoryBTree` coordinates through a shared store that implements just two methods:
|
|
500
|
+
|
|
501
|
+
- **`getLogEntriesSince(version)`** -- returns all mutations since a given version, so each instance can catch up.
|
|
502
|
+
- **`append(expectedVersion, mutations)`** -- atomically appends mutations if the version matches (compare-and-swap). Returns `{ applied, version }`.
|
|
503
|
+
|
|
504
|
+
The store can be backed by anything: an in-memory array, a database table, a Redis stream, etc. Below is a complete in-memory reference implementation.
|
|
505
|
+
|
|
506
|
+
**Node.js / TypeScript:**
|
|
507
|
+
|
|
508
|
+
```ts
|
|
509
|
+
import {
|
|
510
|
+
ConcurrentInMemoryBTree,
|
|
511
|
+
type BTreeMutation,
|
|
512
|
+
type SharedTreeLog,
|
|
513
|
+
type SharedTreeStore,
|
|
514
|
+
} from '@frostpillar/frostpillar-btree';
|
|
515
|
+
|
|
516
|
+
class InMemorySharedStore<TKey, TValue> implements SharedTreeStore<
|
|
517
|
+
TKey,
|
|
518
|
+
TValue
|
|
519
|
+
> {
|
|
520
|
+
private versions: {
|
|
521
|
+
version: bigint;
|
|
522
|
+
mutations: BTreeMutation<TKey, TValue>[];
|
|
523
|
+
}[] = [{ version: 0n, mutations: [] }];
|
|
524
|
+
|
|
525
|
+
public async getLogEntriesSince(
|
|
526
|
+
version: bigint,
|
|
527
|
+
): Promise<SharedTreeLog<TKey, TValue>> {
|
|
528
|
+
const latestVersion = this.versions[this.versions.length - 1].version;
|
|
529
|
+
if (version >= latestVersion) {
|
|
530
|
+
return { version: latestVersion, mutations: [] };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const unseen: BTreeMutation<TKey, TValue>[] = [];
|
|
534
|
+
for (const entry of this.versions) {
|
|
535
|
+
if (entry.version > version) {
|
|
536
|
+
unseen.push(...entry.mutations);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
version: latestVersion,
|
|
542
|
+
mutations: structuredClone(unseen),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
public async append(
|
|
547
|
+
expectedVersion: bigint,
|
|
548
|
+
mutations: BTreeMutation<TKey, TValue>[],
|
|
549
|
+
): Promise<{ applied: boolean; version: bigint }> {
|
|
550
|
+
const latestVersion = this.versions[this.versions.length - 1].version;
|
|
551
|
+
if (latestVersion !== expectedVersion) {
|
|
552
|
+
return { applied: false, version: latestVersion };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const nextVersion = latestVersion + 1n;
|
|
556
|
+
this.versions.push({
|
|
557
|
+
version: nextVersion,
|
|
558
|
+
mutations: structuredClone(mutations),
|
|
559
|
+
});
|
|
560
|
+
return { applied: true, version: nextVersion };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**Browser:**
|
|
566
|
+
|
|
567
|
+
```js
|
|
568
|
+
const { ConcurrentInMemoryBTree } = window.FrostpillarBTree;
|
|
569
|
+
|
|
570
|
+
// Implement SharedTreeStore in the same way.
|
|
571
|
+
// The interface requires two async methods:
|
|
572
|
+
// getLogEntriesSince(version) => { version, mutations }
|
|
573
|
+
// append(expectedVersion, mutations) => { applied, version }
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
#### Creating Coordinated Instances
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
const store = new InMemorySharedStore<number, string>();
|
|
580
|
+
|
|
581
|
+
const instanceA = new ConcurrentInMemoryBTree<number, string>({
|
|
582
|
+
compareKeys: (left: number, right: number): number => left - right,
|
|
583
|
+
enableEntryIdLookup: true,
|
|
584
|
+
store,
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
const instanceB = new ConcurrentInMemoryBTree<number, string>({
|
|
588
|
+
compareKeys: (left: number, right: number): number => left - right,
|
|
589
|
+
enableEntryIdLookup: true,
|
|
590
|
+
store,
|
|
591
|
+
});
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
You can optionally set `maxRetries` (default: 16, minimum: 1, maximum: 1024):
|
|
595
|
+
|
|
596
|
+
```ts
|
|
597
|
+
const instance = new ConcurrentInMemoryBTree<number, string>({
|
|
598
|
+
compareKeys: (a, b) => a - b,
|
|
599
|
+
store,
|
|
600
|
+
maxRetries: 32,
|
|
601
|
+
});
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
You can also set `maxSyncMutationsPerBatch` to cap mutations applied during one `sync` (default: `100000`, minimum: `1`, maximum: `1000000`):
|
|
605
|
+
|
|
606
|
+
```ts
|
|
607
|
+
const hardened = new ConcurrentInMemoryBTree<number, string>({
|
|
608
|
+
compareKeys: (a, b) => a - b,
|
|
609
|
+
store,
|
|
610
|
+
maxSyncMutationsPerBatch: 50000,
|
|
611
|
+
});
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
#### Using the Concurrent API
|
|
615
|
+
|
|
616
|
+
All methods are async. Writes coordinate through the store; reads sync before returning (when `readMode` is `'strong'`, the default).
|
|
617
|
+
|
|
618
|
+
You can set `readMode` to `'local'` to skip sync on reads. In local mode, reads execute against the local tree only and may return stale data. Use explicit `sync()` to catch up:
|
|
619
|
+
|
|
620
|
+
```ts
|
|
621
|
+
const localInstance = new ConcurrentInMemoryBTree<number, string>({
|
|
622
|
+
compareKeys: (a, b) => a - b,
|
|
623
|
+
store,
|
|
624
|
+
readMode: 'local',
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
await localInstance.put(1, 'one');
|
|
628
|
+
await localInstance.sync(); // explicitly pull latest state
|
|
629
|
+
const value = await localInstance.get(1);
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
```ts
|
|
633
|
+
// Instance A inserts
|
|
634
|
+
const insertedId = await instanceA.put(100, 'draft docs');
|
|
635
|
+
|
|
636
|
+
// Instance B can immediately use the same EntryId
|
|
637
|
+
const updated = await instanceB.updateById(insertedId, 'publish docs');
|
|
638
|
+
|
|
639
|
+
// Instance A removes
|
|
640
|
+
const removed = await instanceA.removeById(insertedId);
|
|
641
|
+
|
|
642
|
+
// Instance B syncs and sees the removal
|
|
643
|
+
await instanceB.sync();
|
|
644
|
+
const rows = await instanceB.snapshot(); // []
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
#### Behavior Notes
|
|
648
|
+
|
|
649
|
+
- All instances sharing the same store MUST use identical configuration (`compareKeys`, `duplicateKeys`, `maxLeafEntries`, `maxBranchChildren`, `enableEntryIdLookup`, `autoScale`). The first write appends an `init` mutation with a config fingerprint; other instances validate against it during sync and throw `BTreeConcurrencyError` on mismatch. Comparator consistency remains the caller's responsibility.
|
|
650
|
+
- `EntryId` values are log-derived. When instances share the same store and synchronize, an `EntryId` can be used across instances for `peekById`, `removeById`, and `updateById`.
|
|
651
|
+
- A single instance serializes all async operations (`sync`, reads, writes) to prevent local double-apply.
|
|
652
|
+
- Cross-process guarantees depend on atomic versioned append behavior in the shared store.
|
|
653
|
+
- If a mutation cannot be applied after `maxRetries` attempts, a `BTreeConcurrencyError` is thrown.
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
### Error Handling
|
|
658
|
+
|
|
659
|
+
`@frostpillar/frostpillar-btree` exports three error classes. All extend `Error`.
|
|
660
|
+
|
|
661
|
+
#### BTreeValidationError
|
|
662
|
+
|
|
663
|
+
Thrown when configuration/policy constraints are violated.
|
|
664
|
+
|
|
665
|
+
**Causes:**
|
|
666
|
+
|
|
667
|
+
- `maxLeafEntries` or `maxBranchChildren` is not an integer, less than 3, or greater than 16384
|
|
668
|
+
- `duplicateKeys` is set to an invalid value
|
|
669
|
+
- `put` is called with an existing key when `duplicateKeys` is `'reject'`
|
|
670
|
+
- `removeById`, `peekById`, or `updateById` is called when `enableEntryIdLookup` is `false`
|
|
671
|
+
|
|
672
|
+
```ts
|
|
673
|
+
import {
|
|
674
|
+
BTreeValidationError,
|
|
675
|
+
InMemoryBTree,
|
|
676
|
+
} from '@frostpillar/frostpillar-btree';
|
|
677
|
+
|
|
678
|
+
try {
|
|
679
|
+
const tree = new InMemoryBTree<number, string>({
|
|
680
|
+
compareKeys: (a, b) => a - b,
|
|
681
|
+
duplicateKeys: 'reject',
|
|
682
|
+
});
|
|
683
|
+
tree.put(1, 'one');
|
|
684
|
+
tree.put(1, 'duplicate'); // throws BTreeValidationError
|
|
685
|
+
} catch (error) {
|
|
686
|
+
if (error instanceof BTreeValidationError) {
|
|
687
|
+
console.error('Duplicate key rejected:', error.message);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
#### BTreeInvariantError
|
|
693
|
+
|
|
694
|
+
Thrown by `assertInvariants()` when the B+ tree's internal structure is inconsistent, including comparator reflexivity/transitivity violations. This indicates a bug in the library, a broken comparator contract, or corruption caused by external manipulation.
|
|
695
|
+
|
|
696
|
+
```ts
|
|
697
|
+
import {
|
|
698
|
+
BTreeInvariantError,
|
|
699
|
+
InMemoryBTree,
|
|
700
|
+
} from '@frostpillar/frostpillar-btree';
|
|
701
|
+
|
|
702
|
+
const tree = new InMemoryBTree<number, string>({
|
|
703
|
+
compareKeys: (a, b) => (a === b ? 1 : a - b),
|
|
704
|
+
});
|
|
705
|
+
tree.put(1, 'one');
|
|
706
|
+
|
|
707
|
+
try {
|
|
708
|
+
tree.assertInvariants();
|
|
709
|
+
} catch (error) {
|
|
710
|
+
if (error instanceof BTreeInvariantError) {
|
|
711
|
+
console.error('Tree structure is corrupted:', error.message);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
#### BTreeConcurrencyError
|
|
717
|
+
|
|
718
|
+
Thrown by `ConcurrentInMemoryBTree` when:
|
|
719
|
+
|
|
720
|
+
- A mutation cannot be applied after `maxRetries` retries due to concurrent updates
|
|
721
|
+
- The shared store violates its version contract
|
|
722
|
+
- `maxRetries` is set to an invalid value (not an integer >= 1 and <= 1024)
|
|
723
|
+
- `maxSyncMutationsPerBatch` is set to an invalid value (not an integer >= 1 and <= 1000000)
|
|
724
|
+
- A sync batch exceeds `maxSyncMutationsPerBatch`
|
|
725
|
+
|
|
726
|
+
```ts
|
|
727
|
+
import {
|
|
728
|
+
BTreeConcurrencyError,
|
|
729
|
+
ConcurrentInMemoryBTree,
|
|
730
|
+
type SharedTreeStore,
|
|
731
|
+
} from '@frostpillar/frostpillar-btree';
|
|
732
|
+
|
|
733
|
+
const store: SharedTreeStore<number, string> = {
|
|
734
|
+
async getLogEntriesSince() {
|
|
735
|
+
return { version: 0n, mutations: [] };
|
|
736
|
+
},
|
|
737
|
+
async append() {
|
|
738
|
+
return { applied: true, version: 1n };
|
|
739
|
+
},
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
try {
|
|
743
|
+
new ConcurrentInMemoryBTree<number, string>({
|
|
744
|
+
compareKeys: (a, b) => a - b,
|
|
745
|
+
store,
|
|
746
|
+
maxRetries: 0,
|
|
747
|
+
});
|
|
748
|
+
} catch (error) {
|
|
749
|
+
if (error instanceof BTreeConcurrencyError) {
|
|
750
|
+
console.error('Invalid concurrency config:', error.message);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
---
|
|
756
|
+
|
|
757
|
+
## API Reference
|
|
758
|
+
|
|
759
|
+
### InMemoryBTree
|
|
760
|
+
|
|
761
|
+
| Method | Signature | Description |
|
|
762
|
+
| -------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
763
|
+
| `put` | `(key: TKey, value: TValue) => EntryId` | Insert a key-value pair. Returns an `EntryId`. |
|
|
764
|
+
| `putMany` | `(entries: readonly { key: TKey; value: TValue }[]) => EntryId[]` | Bulk insert pre-sorted entries. O(n) on empty tree; cursor-optimized on non-empty tree. |
|
|
765
|
+
| `remove` | `(key: TKey) => BTreeEntry<TKey, TValue> \| null` | Remove the first matching entry by key. |
|
|
766
|
+
| `removeById` | `(entryId: EntryId) => BTreeEntry<TKey, TValue> \| null` | Remove a specific entry by ID. |
|
|
767
|
+
| `peekById` | `(entryId: EntryId) => BTreeEntry<TKey, TValue> \| null` | Look up an entry by ID without removing it. |
|
|
768
|
+
| `updateById` | `(entryId: EntryId, value: TValue) => BTreeEntry<TKey, TValue> \| null` | Update the value of an entry by ID. |
|
|
769
|
+
| `popFirst` | `() => BTreeEntry<TKey, TValue> \| null` | Remove and return the smallest entry. |
|
|
770
|
+
| `popLast` | `() => BTreeEntry<TKey, TValue> \| null` | Remove and return the largest entry. |
|
|
771
|
+
| `peekFirst` | `() => BTreeEntry<TKey, TValue> \| null` | Return the smallest entry without removing it. |
|
|
772
|
+
| `peekLast` | `() => BTreeEntry<TKey, TValue> \| null` | Return the largest entry without removing it. |
|
|
773
|
+
| `findFirst` | `(key: TKey) => BTreeEntry<TKey, TValue> \| null` | Return the first entry matching key, or null. |
|
|
774
|
+
| `findLast` | `(key: TKey) => BTreeEntry<TKey, TValue> \| null` | Return the last entry matching key, or null. |
|
|
775
|
+
| `get` | `(key: TKey) => TValue \| null` | Return the value of the first matching key, or null. |
|
|
776
|
+
| `hasKey` | `(key: TKey) => boolean` | Check if at least one entry exists for the key. |
|
|
777
|
+
| `count` | `(startKey: TKey, endKey: TKey, options?: RangeBounds) => number` | Count entries in range without array allocation. Bounds default to inclusive. |
|
|
778
|
+
| `range` | `(startKey: TKey, endKey: TKey, options?: RangeBounds) => BTreeEntry<TKey, TValue>[]` | Return entries between startKey and endKey. Bounds default to inclusive. |
|
|
779
|
+
| `nextHigherKey` | `(key: TKey) => TKey \| null` | Return the next key strictly greater than key. |
|
|
780
|
+
| `nextLowerKey` | `(key: TKey) => TKey \| null` | Return the next key strictly less than key. |
|
|
781
|
+
| `getPairOrNextLower` | `(key: TKey) => BTreeEntry<TKey, TValue> \| null` | Return exact match or next lower entry. |
|
|
782
|
+
| `deleteRange` | `(startKey: TKey, endKey: TKey, options?: RangeBounds) => number` | Remove entries in range, return count deleted. |
|
|
783
|
+
| `entries` | `() => IterableIterator<BTreeEntry<TKey, TValue>>` | Lazily iterate all entries in ascending key order. |
|
|
784
|
+
| `entriesReversed` | `() => IterableIterator<BTreeEntry<TKey, TValue>>` | Lazily iterate all entries in descending key order. |
|
|
785
|
+
| `keys` | `() => IterableIterator<TKey>` | Lazily iterate all keys in ascending order. |
|
|
786
|
+
| `values` | `() => IterableIterator<TValue>` | Lazily iterate all values in ascending key order. |
|
|
787
|
+
| `[Symbol.iterator]` | `() => IterableIterator<BTreeEntry<TKey, TValue>>` | Enables `for...of` and spread. Delegates to `entries()`. |
|
|
788
|
+
| `forEach` | `(callback: (entry) => void, thisArg?) => void` | Visit each entry in ascending key order. |
|
|
789
|
+
| `snapshot` | `() => BTreeEntry<TKey, TValue>[]` | Return all entries in sorted order. |
|
|
790
|
+
| `clear` | `() => void` | Remove all entries and reset to empty state in O(1). |
|
|
791
|
+
| `size` | `() => number` | Return the total number of entries. |
|
|
792
|
+
| `getStats` | `() => BTreeStats` | Return structural statistics. |
|
|
793
|
+
| `assertInvariants` | `() => void` | Assert B+ tree structural integrity. Throws if invalid. |
|
|
794
|
+
| `clone` | `() => InMemoryBTree<TKey, TValue>` | Return a structurally independent deep copy. |
|
|
795
|
+
| `toJSON` | `() => BTreeJSON<TKey, TValue>` | Serialize to a versioned JSON-safe payload. |
|
|
796
|
+
| `fromJSON` (static) | `(json, compareKeys) => InMemoryBTree<TKey, TValue>` | Reconstruct a tree from a `toJSON` payload. |
|
|
797
|
+
|
|
798
|
+
**Constructor:**
|
|
799
|
+
|
|
800
|
+
```ts
|
|
801
|
+
new InMemoryBTree<TKey, TValue>(config: InMemoryBTreeConfig<TKey>)
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### ConcurrentInMemoryBTree
|
|
805
|
+
|
|
806
|
+
Exposes a subset of `InMemoryBTree` methods as async equivalents returning `Promise`. Writes coordinate through the shared store; reads sync before returning when `readMode` is `'strong'` (the default). When `readMode` is `'local'`, reads execute against the local tree without syncing. Methods such as `putMany`, iterators, `clear`, `clone`, `deleteRange`, and `toJSON`/`fromJSON` are not yet available on the concurrent wrapper.
|
|
807
|
+
|
|
808
|
+
| Method | Signature | Description |
|
|
809
|
+
| -------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
810
|
+
| `sync` | `() => Promise<void>` | Fetch and apply the latest log entries from the shared store. |
|
|
811
|
+
| `put` | `(key: TKey, value: TValue) => Promise<EntryId>` | Insert with optimistic concurrency. |
|
|
812
|
+
| `remove` | `(key: TKey) => Promise<BTreeEntry<TKey, TValue> \| null>` | Remove the first matching entry by key. |
|
|
813
|
+
| `removeById` | `(entryId: EntryId) => Promise<BTreeEntry<TKey, TValue> \| null>` | Remove a specific entry by ID. |
|
|
814
|
+
| `peekById` | `(entryId: EntryId) => Promise<BTreeEntry<TKey, TValue> \| null>` | Look up an entry by ID (syncs first). |
|
|
815
|
+
| `updateById` | `(entryId: EntryId, value: TValue) => Promise<BTreeEntry<TKey, TValue> \| null>` | Update an entry by ID with optimistic concurrency. |
|
|
816
|
+
| `popFirst` | `() => Promise<BTreeEntry<TKey, TValue> \| null>` | Remove and return the smallest entry. |
|
|
817
|
+
| `popLast` | `() => Promise<BTreeEntry<TKey, TValue> \| null>` | Remove and return the largest entry. |
|
|
818
|
+
| `peekFirst` | `() => Promise<BTreeEntry<TKey, TValue> \| null>` | Return the smallest entry (syncs first). |
|
|
819
|
+
| `peekLast` | `() => Promise<BTreeEntry<TKey, TValue> \| null>` | Return the largest entry (syncs first). |
|
|
820
|
+
| `findFirst` | `(key: TKey) => Promise<BTreeEntry<TKey, TValue> \| null>` | Return the first entry matching key (syncs first). |
|
|
821
|
+
| `findLast` | `(key: TKey) => Promise<BTreeEntry<TKey, TValue> \| null>` | Return the last entry matching key (syncs first). |
|
|
822
|
+
| `get` | `(key: TKey) => Promise<TValue \| null>` | Return value by key (syncs first). |
|
|
823
|
+
| `hasKey` | `(key: TKey) => Promise<boolean>` | Check key existence (syncs first). |
|
|
824
|
+
| `count` | `(startKey: TKey, endKey: TKey, options?: RangeBounds) => Promise<number>` | Count entries in range (syncs first). |
|
|
825
|
+
| `range` | `(startKey: TKey, endKey: TKey, options?: RangeBounds) => Promise<BTreeEntry<TKey, TValue>[]>` | Range query (syncs first). |
|
|
826
|
+
| `nextHigherKey` | `(key: TKey) => Promise<TKey \| null>` | Next key strictly greater (syncs first). |
|
|
827
|
+
| `nextLowerKey` | `(key: TKey) => Promise<TKey \| null>` | Next key strictly less (syncs first). |
|
|
828
|
+
| `getPairOrNextLower` | `(key: TKey) => Promise<BTreeEntry<TKey, TValue> \| null>` | Exact match or next lower (syncs first). |
|
|
829
|
+
| `snapshot` | `() => Promise<BTreeEntry<TKey, TValue>[]>` | Return all entries (syncs first). |
|
|
830
|
+
| `size` | `() => Promise<number>` | Return entry count (syncs first). |
|
|
831
|
+
| `getStats` | `() => Promise<BTreeStats>` | Return structural statistics (syncs first). |
|
|
832
|
+
| `assertInvariants` | `() => Promise<void>` | Assert structural integrity (syncs first). |
|
|
833
|
+
|
|
834
|
+
**Constructor:**
|
|
835
|
+
|
|
836
|
+
```ts
|
|
837
|
+
new ConcurrentInMemoryBTree<TKey, TValue>(config: ConcurrentInMemoryBTreeConfig<TKey, TValue>)
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
### Exported Types
|
|
841
|
+
|
|
842
|
+
| Type | Description |
|
|
843
|
+
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
844
|
+
| `EntryId` | Branded `number` identifying a specific entry. |
|
|
845
|
+
| `BTreeEntry<TKey, TValue>` | `{ entryId: EntryId; key: TKey; value: TValue }` |
|
|
846
|
+
| `BTreeJSON<TKey, TValue>` | Versioned JSON-serializable payload produced by `toJSON()` and consumed by `fromJSON()`. |
|
|
847
|
+
| `BTreeStats` | `{ height: number; leafCount: number; branchCount: number; entryCount: number }` |
|
|
848
|
+
| `KeyComparator<TKey>` | `(left: TKey, right: TKey) => number` |
|
|
849
|
+
| `DuplicateKeyPolicy` | `'allow' \| 'reject' \| 'replace'` |
|
|
850
|
+
| `RangeBounds` | `{ lowerBound?: 'inclusive' \| 'exclusive'; upperBound?: 'inclusive' \| 'exclusive' }` |
|
|
851
|
+
| `InMemoryBTreeConfig<TKey>` | `{ compareKeys: KeyComparator<TKey>; maxLeafEntries?: number; maxBranchChildren?: number; duplicateKeys?: DuplicateKeyPolicy; enableEntryIdLookup?: boolean; autoScale?: boolean }` |
|
|
852
|
+
| `ReadMode` | `'strong' \| 'local'` |
|
|
853
|
+
| `ConcurrentInMemoryBTreeConfig<TKey, TValue>` | Extends `InMemoryBTreeConfig<TKey>` with `store: SharedTreeStore<TKey, TValue>`, `maxRetries?: number`, `maxSyncMutationsPerBatch?: number`, and `readMode?: ReadMode`. |
|
|
854
|
+
| `SharedTreeStore<TKey, TValue>` | Interface with `getLogEntriesSince(version)` and `append(expectedVersion, mutations)`. |
|
|
855
|
+
| `SharedTreeLog<TKey, TValue>` | `{ version: bigint; mutations: BTreeMutation<TKey, TValue>[] }` |
|
|
856
|
+
| `BTreeMutation<TKey, TValue>` | Discriminated union: `init`, `put`, `remove`, `removeById`, `updateById`, `popFirst`, `popLast`. |
|
|
857
|
+
| `BTreeValidationError` | Error thrown for comparator or config violations. |
|
|
858
|
+
| `BTreeInvariantError` | Error thrown for tree structural integrity violations. |
|
|
859
|
+
| `BTreeConcurrencyError` | Error thrown for concurrency conflicts or store contract violations. |
|
|
860
|
+
|
|
861
|
+
> **Subpath exports:** The `/core` subpath (`@frostpillar/frostpillar-btree/core`) exports only single-process types: `InMemoryBTree`, `EntryId`, `BTreeEntry`, `BTreeJSON`, `BTreeStats`, `KeyComparator`, `DuplicateKeyPolicy`, `RangeBounds`, `InMemoryBTreeConfig`, `BTreeValidationError`, and `BTreeInvariantError`. Concurrency-related exports (`ConcurrentInMemoryBTree`, `ConcurrentInMemoryBTreeConfig`, `ReadMode`, `SharedTreeStore`, `SharedTreeLog`, `BTreeMutation`, `BTreeConcurrencyError`) are available only from the main entry point.
|
|
862
|
+
|
|
863
|
+
---
|
|
864
|
+
|
|
865
|
+
## How to Contribute
|
|
866
|
+
|
|
867
|
+
### Prerequisites
|
|
868
|
+
|
|
869
|
+
- Node.js >= 24.0.0
|
|
870
|
+
- pnpm >= 10.0.0
|
|
871
|
+
|
|
872
|
+
### Setup
|
|
873
|
+
|
|
874
|
+
```bash
|
|
875
|
+
git clone https://github.com/hjmsano/frostpillar-btree.git
|
|
876
|
+
cd frostpillar-btree
|
|
877
|
+
pnpm install
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
### Development Commands
|
|
881
|
+
|
|
882
|
+
| Command | Description |
|
|
883
|
+
| --------------------------------------------------------------- | ---------------------------------------------------- |
|
|
884
|
+
| `pnpm build` | Build ESM, CJS, and type declarations into `dist/`. |
|
|
885
|
+
| `pnpm test` | Run all tests. |
|
|
886
|
+
| `pnpm test tests/inMemoryBTree.test.ts` | Run InMemoryBTree tests. |
|
|
887
|
+
| `pnpm test tests/concurrentInMemoryBTree.test.ts` | Run ConcurrentInMemoryBTree tests. |
|
|
888
|
+
| `pnpm test tests/concurrentInMemoryBTree.operations.test.ts` | Run concurrent operations tests. |
|
|
889
|
+
| `pnpm test tests/concurrentInMemoryBTree.storeContract.test.ts` | Run store contract tests. |
|
|
890
|
+
| `pnpm test tests/bundleBuildContract.test.ts` | Run bundle build contract tests. |
|
|
891
|
+
| `pnpm test tests/githubActionsWorkflows.test.ts` | Run workflow contract tests. |
|
|
892
|
+
| `pnpm build:bundle` | Build full browser bundle (includes concurrent API). |
|
|
893
|
+
| `pnpm build:bundle:core` | Build core browser bundle (InMemoryBTree only). |
|
|
894
|
+
| `pnpm bench` | Run benchmarks (run `pnpm build` first). |
|
|
895
|
+
| `pnpm check` | Run typecheck + lint + test + textlint. |
|
|
896
|
+
|
|
897
|
+
### Branch and Release Model
|
|
898
|
+
|
|
899
|
+
- The default branch is `main`.
|
|
900
|
+
- Releases are managed by [Release Please](https://github.com/googleapis/release-please) via `.github/workflows/ci-release.yml`.
|
|
901
|
+
- Merge conventional-commit PRs into the `release` branch. Release Please opens/updates a version-bump PR against `release`.
|
|
902
|
+
- Merging the version-bump PR triggers: GitHub Release creation, browser bundle uploads (`frostpillar-btree.min.js` and `frostpillar-btree-core.min.js`), and GitHub Packages publish.
|
|
903
|
+
|
|
904
|
+
### Documentation
|
|
905
|
+
|
|
906
|
+
- [Docs index](./docs/INDEX.md)
|
|
907
|
+
- [Library spec](./docs/specs/01_in-memory-btree.md)
|
|
908
|
+
- [Release spec](./docs/specs/02_release-driven-cicd-and-publish.md)
|
|
909
|
+
|
|
910
|
+
## License
|
|
911
|
+
|
|
912
|
+
[MIT](./LICENSE)
|