import { LE } from "./buf.ts"; export const PAGE_SIZE = 4096; const HEADER_SIZE = 40; const ROW_GROUP_SIZE = 36; const ROWS_PER_GROUP = 16; function align4(o: number): number { return (o + 3) & ~3; } function numRowGroups(n: number): number { return Math.floor((n + ROWS_PER_GROUP - 1) / ROWS_PER_GROUP); } export class PageBuilder { pageIndex: number; pageType: number; nextPage: number; rowBytes: Uint8Array[] = []; rowOffsets: number[] = []; heapCursor = 0; constructor(pageIndex: number, pageType: number) { this.pageIndex = pageIndex; this.pageType = pageType; this.nextPage = pageIndex; } tryAdd(row: Uint8Array): boolean { const aligned = align4(this.heapCursor); const end = aligned + row.length; const footerAfter = numRowGroups(this.rowBytes.length + 1) * ROW_GROUP_SIZE; if (end + HEADER_SIZE + footerAfter > PAGE_SIZE) return false; this.rowOffsets.push(aligned); this.rowBytes.push(row); this.heapCursor = end; return true; } numRows(): number { return this.rowBytes.length; } finalize(): Uint8Array { const n = this.rowBytes.length; const numRgs = numRowGroups(n); const footerSize = numRgs * ROW_GROUP_SIZE; const heapPadding = PAGE_SIZE - HEADER_SIZE - footerSize; const usedSize = this.heapCursor; const freeSize = heapPadding - usedSize; if (freeSize < 0) throw new Error("page overflow"); const pageFlags = n > 0 ? 0x00 : 0x44; const h = new LE(); h.u32(0); // magic h.u32(this.pageIndex); h.u32(this.pageType); h.u32(this.nextPage); h.u32(0); // unknown1 h.u32(0); // unknown2 h.u8(n & 0xff); h.u8(0); h.u8(0); h.u8(pageFlags); h.u16(freeSize); h.u16(usedSize); h.u16(0); // unknown5 h.u16(n); // num_rows_large h.u16(0); // unknown6 h.u16(0); // unknown7 const header = h.build(); if (header.length !== HEADER_SIZE) throw new Error(`hdr len ${header.length}`); const heap = new Uint8Array(heapPadding); for (let i = 0; i < this.rowBytes.length; i++) { heap.set(this.rowBytes[i]!, this.rowOffsets[i]!); } const footer = new LE(); for (let g = numRgs - 1; g >= 0; g--) { const start = g * ROWS_PER_GROUP; const grpOffsets = this.rowOffsets.slice(start, start + ROWS_PER_GROUP); const grpCount = grpOffsets.length; const padded = grpOffsets.slice(); while (padded.length < ROWS_PER_GROUP) padded.push(0); const presence = (1 << grpCount) - 1; const rg = new LE(); for (let i = padded.length - 1; i >= 0; i--) rg.u16(padded[i]!); rg.u16(presence); rg.u16(16); const rgBytes = rg.build(); if (rgBytes.length !== ROW_GROUP_SIZE) throw new Error("rg size"); footer.bytes(rgBytes); } const footerBytes = footer.build(); if (footerBytes.length !== footerSize) throw new Error("footer size"); const out = new Uint8Array(PAGE_SIZE); out.set(header, 0); out.set(heap, HEADER_SIZE); out.set(footerBytes, HEADER_SIZE + heapPadding); return out; } }