const codeToType = { 0: Int8Array, 1: Uint8Array, 2: Int16Array, 3: Uint16Array, 4: Int32Array, 5: Uint32Array, 7: Float32Array, 8: Float64Array, 9: BigInt64Array, 10: BigUint64Array }; const typeToBytes = { Int8Array: 1, Uint8Array: 1, Int16Array: 2, Uint16Array: 2, Int32Array: 4, Uint32Array: 4, Float32Array: 4, Float64Array: 8, BigInt64Array: 8, BigUint64Array: 8 }; function readTypedValue(view, offset, type) { switch (type) { case Int8Array: return view.getInt8(offset); case Uint8Array: return view.getUint8(offset); case Int16Array: return view.getInt16(offset, true); case Uint16Array: return view.getUint16(offset, true); case Int32Array: return view.getInt32(offset, true); case Uint32Array: return view.getUint32(offset, true); case Float32Array: return view.getFloat32(offset, true); case Float64Array: return view.getFloat64(offset, true); case BigInt64Array: return view.getBigInt64(offset, true); case BigUint64Array: return view.getBigUint64(offset, true); default: throw new Error(`Unsupported type: ${type.name}`); } } class DougalBinaryBundle extends ArrayBuffer { static HEADER_LENGTH = 4; // Length of a bundle header /** Clone an existing ByteArray into a DougalBinaryBundle */ static clone (buffer) { const clone = new DougalBinaryBundle(buffer.byteLength); const uint8Array = new Uint8Array(buffer); const uint8ArrayClone = new Uint8Array(clone); uint8ArrayClone.set(uint8Array); return clone; } constructor (length, options) { super (length, options); } /** Get the count of bundles in this ByteArray. * * Stops at the first non-bundle looking offset */ get bundleCount () { let count = 0; let currentBundleOffset = 0; const view = new DataView(this); while (currentBundleOffset < this.byteLength) { const currentBundleHeader = view.getUint32(currentBundleOffset, true); if ((currentBundleHeader & 0xff) !== 0x1c) { // This is not a bundle return count; } let currentBundleLength = currentBundleHeader >>> 8; currentBundleOffset += currentBundleLength + DougalBinaryBundle.HEADER_LENGTH; count++; } return count; } /** Get the number of chunks in the bundles of this ByteArray */ get chunkCount () { let count = 0; let bundleOffset = 0; const view = new DataView(this); while (bundleOffset < this.byteLength) { const header = view.getUint32(bundleOffset, true); if ((header & 0xFF) !== 0x1C) break; const length = header >>> 8; if (bundleOffset + 4 + length > this.byteLength) break; let chunkOffset = bundleOffset + 4; // relative to buffer start while (chunkOffset < bundleOffset + 4 + length) { const chunkType = view.getUint8(chunkOffset); if (chunkType !== 0x11 && chunkType !== 0x12) break; const cCount = view.getUint16(chunkOffset + 2, true); const ΔelemC = view.getUint8(chunkOffset + 10); const elemC = view.getUint8(chunkOffset + 11); let localOffset = 12; // header size localOffset += ΔelemC + elemC; // preface // initial values for (let k = 0; k < ΔelemC; k++) { const typeByte = view.getUint8(chunkOffset + 12 + k); const baseCode = typeByte & 0xF; const baseType = codeToType[baseCode]; if (!baseType) throw new Error('Invalid base type code'); localOffset += typeToBytes[baseType.name]; } // pad after initial while (localOffset % 4 !== 0) localOffset++; if (chunkType === 0x11) { // Sequential // record data: Δelems incrs for (let k = 0; k < ΔelemC; k++) { const typeByte = view.getUint8(chunkOffset + 12 + k); const incrCode = typeByte >> 4; const incrType = codeToType[incrCode]; if (!incrType) throw new Error('Invalid incr type code'); localOffset += cCount * typeToBytes[incrType.name]; } // elems for (let k = 0; k < elemC; k++) { const typeCode = view.getUint8(chunkOffset + 12 + ΔelemC + k); const type = codeToType[typeCode]; if (!type) throw new Error('Invalid elem type code'); localOffset += cCount * typeToBytes[type.name]; } } else { // Interleaved // Compute exact stride for interleaved record data let ΔelemStride = 0; for (let k = 0; k < ΔelemC; k++) { const typeByte = view.getUint8(chunkOffset + 12 + k); const incrCode = typeByte >> 4; const incrType = codeToType[incrCode]; if (!incrType) throw new Error('Invalid incr type code'); ΔelemStride += typeToBytes[incrType.name]; } let elemStride = 0; for (let k = 0; k < elemC; k++) { const typeCode = view.getUint8(chunkOffset + 12 + ΔelemC + k); const type = codeToType[typeCode]; if (!type) throw new Error('Invalid elem type code'); elemStride += typeToBytes[type.name]; } const recordStride = ΔelemStride + elemStride; localOffset += cCount * recordStride; } // pad after record while (localOffset % 4 !== 0) localOffset++; chunkOffset += localOffset; count++; } bundleOffset += 4 + length; } return count; } /** Return an array of DougalBinaryChunkSequential or DougalBinaryChunkInterleaved instances */ chunks () { const chunks = []; let bundleOffset = 0; const view = new DataView(this); while (bundleOffset < this.byteLength) { const header = view.getUint32(bundleOffset, true); if ((header & 0xFF) !== 0x1C) break; const length = header >>> 8; if (bundleOffset + 4 + length > this.byteLength) break; let chunkOffset = bundleOffset + 4; while (chunkOffset < bundleOffset + 4 + length) { const chunkType = view.getUint8(chunkOffset); if (chunkType !== 0x11 && chunkType !== 0x12) break; const cCount = view.getUint16(chunkOffset + 2, true); const ΔelemC = view.getUint8(chunkOffset + 10); const elemC = view.getUint8(chunkOffset + 11); let localOffset = 12; localOffset += ΔelemC + elemC; // initial values for (let k = 0; k < ΔelemC; k++) { const typeByte = view.getUint8(chunkOffset + 12 + k); const baseCode = typeByte & 0xF; const baseType = codeToType[baseCode]; if (!baseType) throw new Error('Invalid base type code'); localOffset += typeToBytes[baseType.name]; } // pad after initial while (localOffset % 4 !== 0) localOffset++; if (chunkType === 0x11) { // Sequential // record data: Δelems incrs for (let k = 0; k < ΔelemC; k++) { const typeByte = view.getUint8(chunkOffset + 12 + k); const incrCode = typeByte >> 4; const incrType = codeToType[incrCode]; if (!incrType) throw new Error('Invalid incr type code'); localOffset += cCount * typeToBytes[incrType.name]; } // elems for (let k = 0; k < elemC; k++) { const typeCode = view.getUint8(chunkOffset + 12 + ΔelemC + k); const type = codeToType[typeCode]; if (!type) throw new Error('Invalid elem type code'); localOffset += cCount * typeToBytes[type.name]; } } else { // Interleaved // Compute exact stride for interleaved record data let ΔelemStride = 0; for (let k = 0; k < ΔelemC; k++) { const typeByte = view.getUint8(chunkOffset + 12 + k); const incrCode = typeByte >> 4; const incrType = codeToType[incrCode]; if (!incrType) throw new Error('Invalid incr type code'); ΔelemStride += typeToBytes[incrType.name]; } let elemStride = 0; for (let k = 0; k < elemC; k++) { const typeCode = view.getUint8(chunkOffset + 12 + ΔelemC + k); const type = codeToType[typeCode]; if (!type) throw new Error('Invalid elem type code'); elemStride += typeToBytes[type.name]; } const recordStride = ΔelemStride + elemStride; localOffset += cCount * recordStride; } // pad after record while (localOffset % 4 !== 0) localOffset++; switch (chunkType) { case 0x11: chunks.push(new DougalBinaryChunkSequential(this, chunkOffset, localOffset)); break; case 0x12: chunks.push(new DougalBinaryChunkInterleaved(this, chunkOffset, localOffset)); break; default: throw new Error('Invalid chunk type'); } chunkOffset += localOffset; } bundleOffset += 4 + length; } return chunks; } /** Return a ByteArray containing all data from all * chunks including reconstructed i, j and incremental * values as follows: * * // i values (constant) * // j values (j0 + Δj*i) * <Δelem_0_0> <Δelem_0_1> … <Δelem_0_x> // reconstructed Δelem0 (uses baseType) * <Δelem_1_0> <Δelem_1_1> … <Δelem_1_x> // reconstructed Δelem1 * … * <Δelem_y_0> <Δelem_y_1> … <Δelem_y_x> // reconstructed Δelem1 * // First elem * // Second elem * … * // Last elem * * It does not matter whether the underlying chunks are * sequential or interleaved. This function will transform * as necessary. * */ getDataSequentially () { // TODO } /** Return a ByteArray containing all data from all * chunks including reconstructed i, j and incremental * values, interleaved as follows: * * <Δelem_0_0> <Δelem_1_0> … <Δelem_y_0> * <Δelem_0_1> <Δelem_1_1> … <Δelem_y_1> * <Δelem_0_x> <Δelem_1_x> … <Δelem_y_x> * * It does not matter whether the underlying chunks are * sequential or interleaved. This function will transform * as necessary. * */ getDataInterleaved () { // TODO } } class DougalBinaryChunkSequential extends ArrayBuffer { constructor (buffer, offset, length) { super(length); new Uint8Array(this).set(new Uint8Array(buffer, offset, length)); this._ΔelemCaches = new Array(this.ΔelemCount); this._elemCaches = new Array(this.elemCount); this._ΔelemBlockOffsets = null; this._elemBlockOffsets = null; this._recordOffset = null; } _getRecordOffset() { if (this._recordOffset !== null) return this._recordOffset; const view = new DataView(this); const ΔelemC = this.ΔelemCount; const elemC = this.elemCount; let recordOffset = 12 + ΔelemC + elemC; for (let k = 0; k < ΔelemC; k++) { const tb = view.getUint8(12 + k); const bc = tb & 0xF; const bt = codeToType[bc]; recordOffset += typeToBytes[bt.name]; } while (recordOffset % 4 !== 0) recordOffset++; this._recordOffset = recordOffset; return recordOffset; } _initBlockOffsets() { if (this._ΔelemBlockOffsets !== null) return; const view = new DataView(this); const count = this.jCount; const ΔelemC = this.ΔelemCount; const elemC = this.elemCount; const recordOffset = this._getRecordOffset(); this._ΔelemBlockOffsets = []; let o = recordOffset; for (let k = 0; k < ΔelemC; k++) { this._ΔelemBlockOffsets[k] = o; const tb = view.getUint8(12 + k); const ic = tb >> 4; const it = codeToType[ic]; o += count * typeToBytes[it.name]; } this._elemBlockOffsets = []; for (let k = 0; k < elemC; k++) { this._elemBlockOffsets[k] = o; const tc = view.getUint8(12 + ΔelemC + k); const t = codeToType[tc]; o += count * typeToBytes[t.name]; } } /** Return the user-defined value */ get udv () { return new DataView(this).getUint8(1); } /** Return the number of j elements in this chunk */ get jCount () { return new DataView(this).getUint16(2, true); } /** Return the i value in this chunk */ get i () { return new DataView(this).getUint16(4, true); } /** Return the j0 value in this chunk */ get j0 () { return new DataView(this).getUint16(6, true); } /** Return the Δj value in this chunk */ get Δj () { return new DataView(this).getInt16(8, true); } /** Return the Δelem_count value in this chunk */ get ΔelemCount () { return new DataView(this).getUint8(10); } /** Return the elem_count value in this chunk */ get elemCount () { return new DataView(this).getUint8(11); } /** Return a TypedArray (e.g., Uint16Array, …) for the n-th Δelem in the chunk */ Δelem (n) { if (this._ΔelemCaches[n]) return this._ΔelemCaches[n]; if (n < 0 || n >= this.ΔelemCount) throw new Error(`Invalid Δelem index: ${n}`); const view = new DataView(this); const count = this.jCount; const ΔelemC = this.ΔelemCount; const typeByte = view.getUint8(12 + n); const baseCode = typeByte & 0xF; const incrCode = typeByte >> 4; const baseType = codeToType[baseCode]; const incrType = codeToType[incrCode]; if (!baseType || !incrType) throw new Error('Invalid type codes for Δelem'); // Find offset for initial value of this Δelem let initialOffset = 12 + ΔelemC + this.elemCount; for (let k = 0; k < n; k++) { const tb = view.getUint8(12 + k); const bc = tb & 0xF; const bt = codeToType[bc]; initialOffset += typeToBytes[bt.name]; } let current = readTypedValue(view, initialOffset, baseType); // Advance to start of record data (after all initials and pad) const recordOffset = this._getRecordOffset(); // Find offset for deltas of this Δelem (skip previous Δelems' delta blocks) this._initBlockOffsets(); const deltaOffset = this._ΔelemBlockOffsets[n]; // Reconstruct the array const arr = new baseType(count); const isBigInt = baseType === BigInt64Array || baseType === BigUint64Array; arr[0] = current; for (let idx = 1; idx < count; idx++) { let delta = readTypedValue(view, deltaOffset + idx * typeToBytes[incrType.name], incrType); if (isBigInt) { delta = BigInt(delta); current += delta; } else { current += delta; } arr[idx] = current; } this._ΔelemCaches[n] = arr; return arr; } /** Return a TypedArray (e.g., Uint16Array, …) for the n-th elem in the chunk */ elem (n) { if (this._elemCaches[n]) return this._elemCaches[n]; if (n < 0 || n >= this.elemCount) throw new Error(`Invalid elem index: ${n}`); const view = new DataView(this); const count = this.jCount; const ΔelemC = this.ΔelemCount; const elemC = this.elemCount; const typeCode = view.getUint8(12 + ΔelemC + n); const type = codeToType[typeCode]; if (!type) throw new Error('Invalid type code for elem'); // Find offset for this elem's data block this._initBlockOffsets(); const elemOffset = this._elemBlockOffsets[n]; // Create and populate the array const arr = new type(count); const bytes = typeToBytes[type.name]; for (let idx = 0; idx < count; idx++) { arr[idx] = readTypedValue(view, elemOffset + idx * bytes, type); } this._elemCaches[n] = arr; return arr; } getRecord (index) { if (index < 0 || index >= this.jCount) throw new Error(`Invalid record index: ${index}`); const arr = [thid.udv, this.i, this.j0 + index * this.Δj]; for (let m = 0; m < this.ΔelemCount; m++) { const values = this.Δelem(m); arr.push(values[index]); } for (let m = 0; m < this.elemCount; m++) { const values = this.elem(m); arr.push(values[index]); } return arr; } } class DougalBinaryChunkInterleaved extends ArrayBuffer { constructor(buffer, offset, length) { super(length); new Uint8Array(this).set(new Uint8Array(buffer, offset, length)); this._incrStrides = []; this._elemStrides = []; this._incrOffsets = []; this._elemOffsets = []; this._recordStride = 0; this._recordOffset = null; this._initStrides(); this._ΔelemCaches = new Array(this.ΔelemCount); this._elemCaches = new Array(this.elemCount); } _getRecordOffset() { if (this._recordOffset !== null) return this._recordOffset; const view = new DataView(this); const ΔelemC = this.ΔelemCount; const elemC = this.elemCount; let recordOffset = 12 + ΔelemC + elemC; for (let k = 0; k < ΔelemC; k++) { const tb = view.getUint8(12 + k); const bc = tb & 0xF; const bt = codeToType[bc]; recordOffset += typeToBytes[bt.name]; } while (recordOffset % 4 !== 0) recordOffset++; this._recordOffset = recordOffset; return recordOffset; } _initStrides() { const view = new DataView(this); const ΔelemC = this.ΔelemCount; const elemC = this.elemCount; // Compute incr strides and offsets let incrOffset = 0; for (let k = 0; k < ΔelemC; k++) { const typeByte = view.getUint8(12 + k); const incrCode = typeByte >> 4; const incrType = codeToType[incrCode]; if (!incrType) throw new Error('Invalid incr type code'); this._incrOffsets.push(incrOffset); const bytes = typeToBytes[incrType.name]; this._incrStrides.push(bytes); incrOffset += bytes; this._recordStride += bytes; } // Compute elem strides and offsets let elemOffset = incrOffset; for (let k = 0; k < elemC; k++) { const typeCode = view.getUint8(12 + ΔelemC + k); const type = codeToType[typeCode]; if (!type) throw new Error('Invalid elem type code'); this._elemOffsets.push(elemOffset); const bytes = typeToBytes[type.name]; this._elemStrides.push(bytes); elemOffset += bytes; this._recordStride += bytes; } } get udv() { return new DataView(this).getUint8(1); } get jCount() { return new DataView(this).getUint16(2, true); } get i() { return new DataView(this).getUint16(4, true); } get j0() { return new DataView(this).getUint16(6, true); } get Δj() { return new DataView(this).getInt16(8, true); } get ΔelemCount() { return new DataView(this).getUint8(10); } get elemCount() { return new DataView(this).getUint8(11); } Δelem(n) { if (this._ΔelemCaches[n]) return this._ΔelemCaches[n]; if (n < 0 || n >= this.ΔelemCount) throw new Error(`Invalid Δelem index: ${n}`); const view = new DataView(this); const count = this.jCount; const ΔelemC = this.ΔelemCount; const typeByte = view.getUint8(12 + n); const baseCode = typeByte & 0xF; const incrCode = typeByte >> 4; const baseType = codeToType[baseCode]; const incrType = codeToType[incrCode]; if (!baseType || !incrType) throw new Error('Invalid type codes for Δelem'); // Find offset for initial value of this Δelem let initialOffset = 12 + ΔelemC + this.elemCount; for (let k = 0; k < n; k++) { const tb = view.getUint8(12 + k); const bc = tb & 0xF; const bt = codeToType[bc]; initialOffset += typeToBytes[bt.name]; } let current = readTypedValue(view, initialOffset, baseType); // Find offset to start of record data const recordOffset = this._getRecordOffset(); // Use precomputed offset for this Δelem const deltaOffset = recordOffset + this._incrOffsets[n]; // Reconstruct the array const arr = new baseType(count); const isBigInt = baseType === BigInt64Array || baseType === BigUint64Array; arr[0] = current; for (let idx = 1; idx < count; idx++) { let delta = readTypedValue(view, deltaOffset + idx * this._recordStride, incrType); if (isBigInt) { delta = BigInt(delta); current += delta; } else { current += delta; } arr[idx] = current; } this._ΔelemCaches[n] = arr; return arr; } elem(n) { if (this._elemCaches[n]) return this._elemCaches[n]; if (n < 0 || n >= this.elemCount) throw new Error(`Invalid elem index: ${n}`); const view = new DataView(this); const count = this.jCount; const ΔelemC = this.ΔelemCount; const typeCode = view.getUint8(12 + ΔelemC + n); const type = codeToType[typeCode]; if (!type) throw new Error('Invalid type code for elem'); // Find offset to start of record data const recordOffset = this._getRecordOffset(); // Use precomputed offset for this elem (relative to start of record data) const elemOffset = recordOffset + this._elemOffsets[n]; // Create and populate the array const arr = new type(count); const bytes = typeToBytes[type.name]; for (let idx = 0; idx < count; idx++) { arr[idx] = readTypedValue(view, elemOffset + idx * this._recordStride, type); } this._elemCaches[n] = arr; return arr; } getRecord (index) { if (index < 0 || index >= this.jCount) throw new Error(`Invalid record index: ${index}`); const arr = [this.udv, this.i, this.j0 + index * this.Δj]; for (let m = 0; m < this.ΔelemCount; m++) { const values = this.Δelem(m); arr.push(values[index]); } for (let m = 0; m < this.elemCount; m++) { const values = this.elem(m); arr.push(values[index]); } return arr; } } module.exports = { DougalBinaryBundle, DougalBinaryChunkSequential, DougalBinaryChunkInterleaved }