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
3 changes: 2 additions & 1 deletion src/bson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ export {
MaxKey,
BSONRegExp,
Decimal128,
NumberUtils
NumberUtils,
ByteUtils
};
export { BSONValue, bsonType, type BSONTypeTag } from './bson_value';
export { BSONError, BSONVersionError, BSONRuntimeError, BSONOffsetError } from './error';
Expand Down
7 changes: 6 additions & 1 deletion src/utils/byte_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export type ByteUtils = {
allocate: (size: number) => Uint8Array;
/** Create empty space of size, use pooled memory when available */
allocateUnsafe: (size: number) => Uint8Array;
/** Compare 2 Uint8Arrays lexicographically */
compare: (buffer1: Uint8Array, buffer2: Uint8Array) => -1 | 0 | 1;
/** Concatenating all the Uint8Arrays in new Uint8Array. */
concat: (list: Uint8Array[]) => Uint8Array;
/** Check if two Uint8Arrays are deep equal */
equals: (a: Uint8Array, b: Uint8Array) => boolean;
/** Check if two Uint8Arrays are deep equal */
Expand Down Expand Up @@ -58,6 +62,7 @@ const hasGlobalBuffer = typeof Buffer === 'function' && Buffer.prototype?._isBuf
* The type annotation is important here, it asserts that each of the platform specific
* utils implementations are compatible with the common one.
*
* @internal
* @public
* @experimental
*/
export const ByteUtils: ByteUtils = hasGlobalBuffer ? nodeJsByteUtils : webByteUtils;
15 changes: 14 additions & 1 deletion src/utils/node_byte_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type NodeJsBuffer = ArrayBufferView &
toString: (this: Uint8Array, encoding: NodeJsEncoding, start?: number, end?: number) => string;
equals: (this: Uint8Array, other: Uint8Array) => boolean;
swap32: (this: NodeJsBuffer) => NodeJsBuffer;
compare: (this: Uint8Array, other: Uint8Array) => -1 | 0 | 1;
};
type NodeJsBufferConstructor = Omit<Uint8ArrayConstructor, 'from'> & {
alloc: (size: number) => NodeJsBuffer;
Expand All @@ -21,6 +22,7 @@ type NodeJsBufferConstructor = Omit<Uint8ArrayConstructor, 'from'> & {
from(base64: string, encoding: NodeJsEncoding): NodeJsBuffer;
byteLength(input: string, encoding: 'utf8'): number;
isBuffer(value: unknown): value is NodeJsBuffer;
concat(list: Uint8Array[]): NodeJsBuffer;
};

// This can be nullish, but we gate the nodejs functions on being exported whether or not this exists
Expand Down Expand Up @@ -51,7 +53,10 @@ const nodejsRandomBytes = (() => {
}
})();

/** @internal */
/**
* @public
* @experimental
*/
export const nodeJsByteUtils = {
toLocalBufferType(potentialBuffer: Uint8Array | NodeJsBuffer | ArrayBuffer): NodeJsBuffer {
if (Buffer.isBuffer(potentialBuffer)) {
Expand Down Expand Up @@ -88,6 +93,14 @@ export const nodeJsByteUtils = {
return Buffer.allocUnsafe(size);
},

compare(a: Uint8Array, b: Uint8Array) {
return nodeJsByteUtils.toLocalBufferType(a).compare(b);
},

concat(list: Uint8Array[]): NodeJsBuffer {
return Buffer.concat(list);
},

equals(a: Uint8Array, b: Uint8Array): boolean {
return nodeJsByteUtils.toLocalBufferType(a).equals(b);
},
Expand Down
41 changes: 40 additions & 1 deletion src/utils/web_byte_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ const webRandomBytes: (byteLength: number) => Uint8Array = (() => {

const HEX_DIGIT = /(\d|[a-f])/i;

/** @internal */
/**
* @public
* @experimental
*/
export const webByteUtils = {
toLocalBufferType(
potentialUint8array: Uint8Array | ArrayBufferViewWithTag | ArrayBuffer
Expand Down Expand Up @@ -114,6 +117,42 @@ export const webByteUtils = {
return webByteUtils.allocate(size);
},

compare(a: Uint8Array, b: Uint8Array): -1 | 0 | 1 {
if (a === b) return 0;

const len = Math.min(a.length, b.length);

for (let i = 0; i < len; i++) {
if (a[i] < b[i]) return -1;
if (a[i] > b[i]) return 1;
}

if (a.length < b.length) return -1;
if (a.length > b.length) return 1;

return 0;
},

concat(list: Uint8Array[]): Uint8Array {
Copy link
Contributor

@baileympearson baileympearson Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
concat(list: Uint8Array[]): Uint8Array {
concat(uint8Arrays: Uint8Array[]): Uint8Array {

or

Suggested change
concat(list: Uint8Array[]): Uint8Array {
concat(arraysToConcat: Uint8Array[]): Uint8Array {

or something.

strive for as descriptive variable names as possible

if (list.length === 0) return webByteUtils.allocate(0);
if (list.length === 1) return list[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very clever optimization, but I wonder if returning the input instance could lead to small surprises when BSON is running using this "web mode" implementation.

In Node.js a copy to a new buffer is always performed


let totalLength = 0;
for (const arr of list) {
totalLength += arr.length;
}

const result = webByteUtils.allocate(totalLength);
let offset = 0;

for (const arr of list) {
Copy link
Contributor

@baileympearson baileympearson Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (const arr of list) {
for (const uint8Array of uint8Arrays) {

same as above

result.set(arr, offset);
offset += arr.length;
}

return result;
},

equals(a: Uint8Array, b: Uint8Array): boolean {
if (a.byteLength !== b.byteLength) {
return false;
Expand Down
121 changes: 120 additions & 1 deletion test/node/byte_utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,123 @@ const swap32Tests: ByteUtilTest<'swap32'>[] = [
}
}
];
const compareTests: ByteUtilTest<'compare'>[] = [
{
name: 'returns 0 for two equal arrays (same content)',
inputs: [new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3])],
expectation({ output }) {
expect(output).to.equal(0);
}
},
{
name: 'returns 0 when comparing the same buffer by reference',
inputs: (() => {
const buf = new Uint8Array([5, 6, 7]);
return [buf, buf];
})(),
expectation({ output }) {
expect(output).to.equal(0);
}
},
{
name: 'array a is lexicographically less than array b (first differing byte)',
inputs: [new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 4])],
expectation({ output }) {
expect(output).to.equal(-1);
}
},
{
name: 'array a is lexicographically greater than array b (first differing byte)',
inputs: [new Uint8Array([1, 2, 4]), new Uint8Array([1, 2, 3])],
expectation({ output }) {
expect(output).to.equal(1);
}
},
{
name: 'a is a strict prefix of b (a shorter, same starting bytes) -> a < b',
inputs: [new Uint8Array([1, 2]), new Uint8Array([1, 2, 3])],
expectation({ output }) {
expect(output).to.equal(-1);
}
},
{
name: 'b is a strict prefix of a (b shorter, same starting bytes) -> a > b',
inputs: [new Uint8Array([1, 2, 3]), new Uint8Array([1, 2])],
expectation({ output }) {
expect(output).to.equal(1);
}
},
{
name: 'handles empty arrays',
inputs: [new Uint8Array([]), new Uint8Array([])],
expectation({ output }) {
expect(output).to.equal(0);
}
}
];
const concatTests: ByteUtilTest<'concat'>[] = [
{
name: 'concatenates two non-empty arrays',
inputs: [[new Uint8Array([1, 2]), new Uint8Array([3, 4])]],
expectation({ output, error }) {
expect(error).to.be.null;
expect(output).to.deep.equal(Buffer.from([1, 2, 3, 4]));
}
},
{
name: 'concatenates multiple arrays in order',
inputs: [[new Uint8Array([1]), new Uint8Array([2, 3]), new Uint8Array([4, 5, 6])]],
expectation({ output, error }) {
expect(error).to.be.null;
expect(output).to.deep.equal(Buffer.from([1, 2, 3, 4, 5, 6]));
}
},
{
name: 'returns an empty Uint8Array when given an empty list',
inputs: [[]],
expectation({ output, error }) {
expect(error).to.be.null;
expect(output).to.have.property('byteLength', 0);
expect(output).to.deep.equal(Buffer.from([]));
}
},
{
name: 'returns the same contents when given a single array',
inputs: [[new Uint8Array([7, 8, 9])]],
expectation({ output, error }) {
expect(error).to.be.null;
expect(output).to.deep.equal(Buffer.from([7, 8, 9]));
}
},
{
name: 'handles concatenation with empty arrays inside the list',
inputs: [
[new Uint8Array([]), new Uint8Array([1, 2, 3]), new Uint8Array([]), new Uint8Array([4])]
],
expectation({ output, error }) {
expect(error).to.be.null;
expect(output).to.deep.equal(Buffer.from([1, 2, 3, 4]));
}
},
{
name: 'result has correct total byteLength',
inputs: [[new Uint8Array([1, 2]), new Uint8Array([3]), new Uint8Array([4, 5, 6])]],
expectation({ output, error }) {
expect(error).to.be.null;
// 2 + 1 + 3 = 6
expect(output).to.have.property('byteLength', 6);
expect(output).to.deep.equal(Buffer.from([1, 2, 3, 4, 5, 6]));
}
},
{
name: 'concatenates arrays with overlapping contents correctly',
inputs: [[new Uint8Array([0, 0, 1]), new Uint8Array([1, 0, 0])]],
expectation({ output, error }) {
expect(error).to.be.null;
expect(output).to.deep.equal(Buffer.from([0, 0, 1, 1, 0, 0]));
}
}
];

const utils = new Map([
['nodeJsByteUtils', nodeJsByteUtils],
Expand All @@ -538,7 +655,9 @@ const table = new Map<keyof ByteUtils, ByteUtilTest<keyof ByteUtils>[]>([
['toUTF8', toUTF8Tests],
['utf8ByteLength', utf8ByteLengthTests],
['randomBytes', randomBytesTests],
['swap32', swap32Tests]
['swap32', swap32Tests],
['compare', compareTests],
['concat', concatTests]
]);

describe('ByteUtils', () => {
Expand Down
3 changes: 2 additions & 1 deletion test/node/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ const EXPECTED_EXPORTS = [
'deserializeStream',
'BSON',
'bsonType',
'NumberUtils'
'NumberUtils',
'ByteUtils'
];

const EXPECTED_EJSON_EXPORTS = ['parse', 'stringify', 'serialize', 'deserialize'];
Expand Down
Loading