@pgxsinkit/pgwasm
Version:
94 lines (81 loc) • 3.1 kB
text/typescript
// Began as a copy of `@electric-sql/pg-protocol`, itself adapted from node-postgres' `pg-protocol`
// (MIT, © Brian Carlson; ElectricSQL's changes taken under the PostgreSQL License — see NOTICE).
// Owned outright (ADR-0062); compatibility with PGlite is an anti-goal — evolve freely.
import { byteLengthUtf8 } from "./string-utils";
/** Builds frontend messages: a 5-byte header slot (code + length) followed by the body. */
export class Writer {
readonly #size: number;
#bufferView: DataView<ArrayBuffer>;
#offset = 5;
readonly #encoder = new TextEncoder();
readonly #headerPosition = 0;
constructor(size = 256) {
this.#size = size;
this.#bufferView = this.#allocateBuffer(size);
}
#allocateBuffer(size: number): DataView<ArrayBuffer> {
return new DataView(new ArrayBuffer(size));
}
#ensure(size: number): void {
const remaining = this.#bufferView.byteLength - this.#offset;
if (remaining < size) {
const oldBuffer = this.#bufferView.buffer;
// Grow by ~1.5x (https://stackoverflow.com/questions/2269063/buffer-growth-strategy).
const newSize = oldBuffer.byteLength + (oldBuffer.byteLength >> 1) + size;
this.#bufferView = this.#allocateBuffer(newSize);
new Uint8Array(this.#bufferView.buffer).set(new Uint8Array(oldBuffer));
}
}
addInt32(num: number): Writer {
this.#ensure(4);
this.#bufferView.setInt32(this.#offset, num, false);
this.#offset += 4;
return this;
}
addInt16(num: number): Writer {
this.#ensure(2);
this.#bufferView.setInt16(this.#offset, num, false);
this.#offset += 2;
return this;
}
addCString(string: string): Writer {
if (string) {
this.addString(string);
}
this.#ensure(1);
this.#bufferView.setUint8(this.#offset, 0);
this.#offset++;
return this;
}
addString(string = ""): Writer {
const length = byteLengthUtf8(string);
this.#ensure(length);
this.#encoder.encodeInto(string, new Uint8Array(this.#bufferView.buffer, this.#offset));
this.#offset += length;
return this;
}
add(otherBuffer: ArrayBuffer | ArrayBufferView): Writer {
const bytes = ArrayBuffer.isView(otherBuffer)
? new Uint8Array(otherBuffer.buffer, otherBuffer.byteOffset, otherBuffer.byteLength)
: new Uint8Array(otherBuffer);
this.#ensure(bytes.byteLength);
new Uint8Array(this.#bufferView.buffer).set(bytes, this.#offset);
this.#offset += bytes.byteLength;
return this;
}
#join(code?: number): ArrayBuffer {
if (code) {
this.#bufferView.setUint8(this.#headerPosition, code);
// The length counts everything in the message but the code byte.
const length = this.#offset - (this.#headerPosition + 1);
this.#bufferView.setInt32(this.#headerPosition + 1, length, false);
}
return this.#bufferView.buffer.slice(code ? 0 : 5, this.#offset);
}
flush(code?: number): Uint8Array<ArrayBuffer> {
const result = this.#join(code);
this.#offset = 5;
this.#bufferView = this.#allocateBuffer(this.#size);
return new Uint8Array(result);
}
}