-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
bc7d5c0
commit 8c9c51c
Showing
2 changed files
with
306 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import { BPTreeNode, MemoryPointer, compareBytes } from "./node"; | ||
|
||
// taken from `buffer.go` | ||
interface ReadWriteSeekTruncater { | ||
write(buffer: Uint8Array): Promise<number>; | ||
seek(offset: number, whence: "start" | "current" | "end"): Promise<number>; | ||
read(buffer: Uint8Array, offset: number): Promise<number>; | ||
truncate(size: number): Promise<void>; | ||
} | ||
|
||
interface MetaPage { | ||
root(): Promise<MemoryPointer>; | ||
setRoot(pointer: MemoryPointer): void; | ||
} | ||
|
||
class BPTree { | ||
private tree: ReadWriteSeekTruncater; | ||
private meta: MetaPage; | ||
private maxPageSize: number; | ||
|
||
constructor( | ||
tree: ReadWriteSeekTruncater, | ||
meta: MetaPage, | ||
maxPageSize: number | ||
) { | ||
this.tree = tree; | ||
this.meta = meta; | ||
this.maxPageSize = maxPageSize; | ||
} | ||
|
||
private async root(): Promise<[BPTreeNode | null, MemoryPointer]> { | ||
const mp = await this.meta.root(); | ||
if (!mp || mp.length === 0) { | ||
return [null, mp]; | ||
} | ||
|
||
const root = await this.readNode(mp); | ||
if (!root) { | ||
return [null, mp]; | ||
} | ||
|
||
return [root, mp]; | ||
} | ||
|
||
private async readNode(ptr: MemoryPointer): Promise<BPTreeNode | null> { | ||
const pos = await this.tree.seek(ptr.offset, "start"); | ||
if (!pos || pos !== ptr.offset) { | ||
return null; | ||
} | ||
|
||
const buffer = Buffer.alloc(ptr.length); | ||
await this.tree.read(buffer, 0); | ||
|
||
const node = new BPTreeNode(this.tree, [], []); | ||
|
||
const bytesRead = await node.readFrom(buffer); | ||
|
||
if (!bytesRead || bytesRead !== ptr.length) { | ||
return null; | ||
} | ||
|
||
return node; | ||
} | ||
|
||
private async traverse( | ||
key: Uint8Array, | ||
node: BPTreeNode | ||
): Promise<TraversalRecord[] | null> { | ||
if (await node.leaf()) { | ||
return [{ node: node, index: 0 }]; | ||
} | ||
|
||
for (const [i, k] of node.keys.entries()) { | ||
if (compareBytes(key, k.value) < 0) { | ||
const child = await this.readNode(node.pointers[i]); | ||
if (!child) { | ||
return null; | ||
} | ||
|
||
const path = await this.traverse(key, child); | ||
if (!path) { | ||
return null; | ||
} | ||
|
||
return [...path, { node: node, index: i }]; | ||
} | ||
} | ||
|
||
const child = await this.readNode(node.pointers[node.pointers.length - 1]); | ||
|
||
if (!child) { | ||
return null; | ||
} | ||
|
||
const path = await this.traverse(key, child); | ||
if (!path) { | ||
return null; | ||
} | ||
|
||
return [...path, { node: node, index: node.keys.length }]; | ||
} | ||
|
||
public async find(key: Uint8Array): Promise<[MemoryPointer, boolean]> { | ||
let [rootNode, _] = await this.root(); | ||
|
||
if (!rootNode) { | ||
return [new MemoryPointer(0, 0), false]; | ||
} | ||
|
||
const path = await this.traverse(key, rootNode); | ||
if (!path) { | ||
return [new MemoryPointer(0, 0), false]; | ||
} | ||
|
||
const n = path[0].node; | ||
|
||
const [i, found] = await n.bsearch(key); | ||
|
||
if (found) { | ||
return [n.pointers[i], true]; | ||
} | ||
|
||
return [new MemoryPointer(0, 0), false]; | ||
} | ||
} | ||
|
||
class TraversalRecord { | ||
public node: BPTreeNode; | ||
public index: number; | ||
|
||
constructor(node: BPTreeNode, index: number) { | ||
this.node = node; | ||
this.index = index; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
class ReferencedValue { | ||
private dataPointer: MemoryPointer; | ||
public value: Buffer; | ||
|
||
constructor(dataPointer: MemoryPointer, value: Buffer) { | ||
this.dataPointer = dataPointer; | ||
this.value = value; | ||
} | ||
} | ||
|
||
export class MemoryPointer { | ||
offset: number; | ||
length: number; | ||
|
||
constructor(offset: number, length: number) { | ||
this.offset = offset; | ||
this.length = length; | ||
} | ||
} | ||
|
||
interface DataHandler { | ||
read(buffer: Uint8Array, offset: number): Promise<number>; | ||
} | ||
|
||
export class BPTreeNode { | ||
public dataHandler: DataHandler; | ||
public pointers: MemoryPointer[]; | ||
public keys: ReferencedValue[]; | ||
|
||
constructor( | ||
dataHandler: DataHandler, | ||
pointers: MemoryPointer[], | ||
keys: ReferencedValue[] | ||
) { | ||
this.dataHandler = dataHandler; | ||
this.pointers = pointers; | ||
this.keys = keys; | ||
} | ||
|
||
addPointer(pointer: MemoryPointer) { | ||
this.pointers.push(pointer); | ||
} | ||
|
||
addKey(key: ReferencedValue) { | ||
this.keys.push(key); | ||
} | ||
|
||
async readAtOffset(buffer: Uint8Array, offset: number): Promise<number> { | ||
return await this.dataHandler.read(buffer, offset); | ||
} | ||
|
||
async leaf(): Promise<boolean> { | ||
return this.pointers.length === this.keys.length; | ||
} | ||
|
||
async readFrom(buffer: Buffer): Promise<number> { | ||
let offset = 0; | ||
let size: number; | ||
|
||
let m = 4; | ||
|
||
try { | ||
// since we are reading a 32-bit integer, we move by 4 bytes | ||
size = buffer.readInt32BE(offset); | ||
offset += 4; | ||
|
||
const leaf = size < 0; | ||
const absSize = Math.abs(size); | ||
|
||
this.pointers = new Array(absSize + (leaf ? 0 : 1)) | ||
.fill(null) | ||
.map(() => new MemoryPointer(0, 0)); | ||
|
||
this.keys = new Array(absSize) | ||
.fill(null) | ||
.map( | ||
() => new ReferencedValue(new MemoryPointer(0, 0), Buffer.alloc(0)) | ||
); | ||
|
||
for (let idx = 0; idx <= this.keys.length - 1; idx++) { | ||
const l = buffer.readUInt32BE(offset); | ||
|
||
offset += 4; | ||
m += 4; | ||
|
||
if (l == 0) { | ||
const dpOffset = buffer.readUInt32BE(offset); | ||
offset += 4; | ||
const dpLength = buffer.readUInt32BE(offset); | ||
offset += 4; | ||
|
||
const dataPointer = new MemoryPointer(dpOffset, dpLength); | ||
const keyValue = Buffer.alloc(dpLength); | ||
|
||
await this.dataHandler.read(keyValue, dpOffset); | ||
this.keys[idx] = new ReferencedValue(dataPointer, keyValue); | ||
m += 12; | ||
} else { | ||
const keyValue = buffer.slice(offset, offset + l); | ||
this.keys[idx] = new ReferencedValue( | ||
new MemoryPointer(0, 0), | ||
keyValue | ||
); | ||
|
||
offset += l; | ||
m += l; | ||
} | ||
} | ||
|
||
for (let idx = 0; idx <= this.pointers.length - 1; idx++) { | ||
const pointerOffset = buffer.readUint32BE(offset); | ||
offset += 4; | ||
|
||
const pointerLength = buffer.readUint32BE(offset); | ||
offset += 4; | ||
|
||
this.pointers[idx] = new MemoryPointer(pointerOffset, pointerLength); | ||
|
||
m += 8; | ||
} | ||
} catch (error) { | ||
return 0; | ||
} | ||
return m; | ||
} | ||
|
||
async bsearch(key: Uint8Array): Promise<[number, boolean]> { | ||
let lo = 0; | ||
let hi = this.keys.length - 1; | ||
|
||
while (lo <= hi) { | ||
const mid = (lo + hi) / 2; | ||
const cmp = compareBytes(key, this.keys[mid].value); | ||
|
||
switch (cmp) { | ||
case 0: | ||
return [mid, true]; | ||
case -1: | ||
hi = mid - 1; | ||
case 1: | ||
lo = mid + 1; | ||
} | ||
} | ||
|
||
return [lo, false]; | ||
} | ||
} | ||
|
||
// https://pkg.go.dev/internal/bytealg#Compare | ||
export function compareBytes(a: Uint8Array, b: Uint8Array): number { | ||
const len = Math.min(a.length, b.length); | ||
|
||
for (let idx = 0; idx <= len - 1; idx++) { | ||
if (a[idx] !== b[idx]) { | ||
return -1; | ||
} | ||
|
||
if (a[idx] > b[idx]) { | ||
return 1; | ||
} | ||
} | ||
|
||
if (a.length < b.length) { | ||
return -1; | ||
} | ||
if (a.length > b.length) { | ||
return 1; | ||
} | ||
|
||
return 0; | ||
} |