102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
import type { DumpColumns } from './dump-parse'
|
|
|
|
/**
|
|
* A parsed dump serialised as one flat file.
|
|
*
|
|
* [0..4) magic
|
|
* [4..8) manifest length in bytes
|
|
* [8..12) payload base offset
|
|
* [12..16) reserved
|
|
* [16..) JSON manifest, then zero padding, then the column buffers
|
|
*
|
|
* The payload offset lives in the fixed header rather than the manifest so
|
|
* that writing it cannot change the manifest's own length.
|
|
*
|
|
* This exists for the case where the dump ships with the site instead of
|
|
* being uploaded. Parse once at build time and the browser does one fetch
|
|
* plus a few typed-array views over the response - no parsing, no IndexedDB.
|
|
* It also gzips far better than JSON, because each column is homogeneous.
|
|
*/
|
|
|
|
// Bumped to \x02 when `kind` and `value` were added; a \x01 bundle has no way
|
|
// to tell enum constants from objects, so it is rejected rather than upgraded.
|
|
const MAGIC = 0x50445802 // "PDX\x02"
|
|
const MAGIC_V1 = 0x50445801
|
|
const HEADER = 16
|
|
|
|
const COLUMNS = [
|
|
['addr', Float64Array],
|
|
['typeId', Uint16Array],
|
|
['pathStart', Uint32Array],
|
|
['pathBlob', Uint8Array],
|
|
['nameOff', Uint16Array],
|
|
['rootOff', Uint16Array],
|
|
['outer', Float64Array],
|
|
['offset', Int32Array],
|
|
['kind', Uint8Array],
|
|
['value', BigInt64Array],
|
|
] as const
|
|
|
|
const align8 = (n: number) => (8 - (n % 8)) % 8
|
|
|
|
export function encodeBundle(cols: DumpColumns): Uint8Array {
|
|
const layout: Record<string, { offset: number; length: number }> = {}
|
|
const parts: { at: number; bytes: Uint8Array }[] = []
|
|
let size = 0
|
|
|
|
for (const [key] of COLUMNS) {
|
|
const view = cols[key] as ArrayBufferView & { length: number }
|
|
size += align8(size)
|
|
layout[key] = { offset: size, length: view.length }
|
|
parts.push({
|
|
at: size,
|
|
bytes: new Uint8Array(view.buffer, view.byteOffset, view.byteLength),
|
|
})
|
|
size += view.byteLength
|
|
}
|
|
|
|
const json = new TextEncoder().encode(
|
|
JSON.stringify({
|
|
count: cols.count,
|
|
types: cols.types,
|
|
skipped: cols.skipped,
|
|
damaged: cols.damaged,
|
|
columns: layout,
|
|
}),
|
|
)
|
|
const base = HEADER + json.length + align8(HEADER + json.length)
|
|
|
|
const out = new Uint8Array(base + size)
|
|
const dv = new DataView(out.buffer)
|
|
dv.setUint32(0, MAGIC, true)
|
|
dv.setUint32(4, json.length, true)
|
|
dv.setUint32(8, base, true)
|
|
out.set(json, HEADER)
|
|
for (const { at, bytes } of parts) out.set(bytes, base + at)
|
|
return out
|
|
}
|
|
|
|
export function decodeBundle(buf: ArrayBuffer): DumpColumns {
|
|
const dv = new DataView(buf)
|
|
const magic = dv.getUint32(0, true)
|
|
if (magic === MAGIC_V1) {
|
|
throw new Error('This .pdx was built by an older parser. Re-run `npm run prebake`.')
|
|
}
|
|
if (magic !== MAGIC) throw new Error('Not a .pdx bundle.')
|
|
const jsonLen = dv.getUint32(4, true)
|
|
const base = dv.getUint32(8, true)
|
|
const manifest = JSON.parse(new TextDecoder().decode(new Uint8Array(buf, HEADER, jsonLen)))
|
|
|
|
const cols: Record<string, unknown> = {
|
|
count: manifest.count,
|
|
types: manifest.types,
|
|
skipped: manifest.skipped,
|
|
damaged: manifest.damaged ?? 0,
|
|
}
|
|
for (const [key, Ctor] of COLUMNS) {
|
|
const { offset, length } = manifest.columns[key]
|
|
cols[key] = new Ctor(buf, base + offset, length)
|
|
}
|
|
return cols as unknown as DumpColumns
|
|
}
|