Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion crates/bindings-typescript/src/lib/binary_reader.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Shared decoder: `readString` is called once per string column per row.
const textDecoder = new TextDecoder('utf-8');

export default class BinaryReader {
/**
* The DataView used to read values from the binary data.
Expand Down Expand Up @@ -196,6 +199,6 @@ export default class BinaryReader {
// A view is safe here: TextDecoder copies the bytes synchronously, so nothing
// retains a reference to the reader's buffer. Avoids readUInt8Array's copy.
const bytes = this.readBytes(length);
return new TextDecoder('utf-8').decode(bytes);
return textDecoder.decode(bytes);
}
}
27 changes: 24 additions & 3 deletions crates/bindings-typescript/src/lib/binary_writer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { fromByteArray } from 'base64-js';

// Shared encoder: `writeString` is called once per string column per row.
const textEncoder = new TextEncoder();

const ArrayBufferPrototypeTransfer =
ArrayBuffer.prototype.transfer ??
function (this: ArrayBuffer, newByteLength) {
Expand Down Expand Up @@ -206,8 +209,26 @@ export default class BinaryWriter {
}

writeString(value: string): void {
const encoder = new TextEncoder();
const encodedString = encoder.encode(value);
this.writeUInt8Array(encodedString);
// Fast path: pure-ASCII strings are written straight into the buffer,
// one byte per char, with no intermediate allocation. Non-ASCII input
// falls back to the shared encoder. This method runs once per string
// column per row on every insert/update, so per-call allocations
// (a fresh TextEncoder and a temporary Uint8Array) dominated its cost.
const len = value.length;
this.expandBuffer(4 + len);
const bytes = new Uint8Array(this.buffer.buffer);
let offset = this.offset + 4;
let i = 0;
for (; i < len; i++) {
const c = value.charCodeAt(i);
if (c >= 0x80) break;
bytes[offset++] = c;
}
if (i === len) {
this.view.setUint32(this.offset, len, true);
this.offset = offset;
return;
}
this.writeUInt8Array(textEncoder.encode(value));
}
}
57 changes: 57 additions & 0 deletions crates/bindings-typescript/tests/binary_read_write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,60 @@ describe('readUInt8Array buffer ownership', () => {
expect(value).toBe('héllo ☃');
});
});

describe('writeString', () => {
// BSATN layout for `string`: u32 byte length (LE) followed by UTF-8 bytes.
const expected = (value: string): number[] => {
const utf8 = new TextEncoder().encode(value);
return [
utf8.length & 0xff,
(utf8.length >> 8) & 0xff,
(utf8.length >> 16) & 0xff,
(utf8.length >> 24) & 0xff,
...utf8,
];
};
const written = (value: string, initialCapacity = 8): number[] => {
const writer = new BinaryWriter(initialCapacity);
writer.writeString(value);
return [...writer.getBuffer()];
};

test.each([
['empty', ''],
['ascii', 'hello world'],
['ascii at the fast-path boundary', '\x7f'],
['latin-1', 'héllo'],
['cjk', '日本語'],
['astral (surrogate pair)', 'emoji 🎉 mix'],
['ascii prefix then non-ascii', 'ascii then ÿ'],
['long ascii', 'x'.repeat(10_000)],
['long non-ascii', 'ü'.repeat(10_000)],
])('%s encodes like TextEncoder', (_name, value) => {
expect(written(value)).toEqual(expected(value));
});

test('grows the buffer from a tiny initial capacity', () => {
expect(written('hello world', 1)).toEqual(expected('hello world'));
expect(written('héllo', 1)).toEqual(expected('héllo'));
});

test('round-trips through readString', () => {
for (const value of ['', 'plain', 'héllo ☃', '🎉'.repeat(100)]) {
const writer = new BinaryWriter(4);
writer.writeString(value);
expect(new BinaryReader(writer.getBuffer()).readString()).toBe(value);
}
});

test('consecutive writes stay contiguous', () => {
const writer = new BinaryWriter(4);
writer.writeString('ab');
writer.writeString('ç');
writer.writeU8(7);
const reader = new BinaryReader(writer.getBuffer());
expect(reader.readString()).toBe('ab');
expect(reader.readString()).toBe('ç');
expect(reader.readU8()).toBe(7);
});
});