Skip to content
Merged
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: 5 additions & 0 deletions .changeset/lovely-carpets-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hsblabs/web-stream-extras": patch
---

refactor: replace ByteQueue class with createByteQueue function for improved instantiation
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"files": {
"ignoreUnknown": false,
"includes": ["**"]
"includes": ["**", "!.agents", "!.claude"]
},
"formatter": {
"enabled": true,
Expand Down
16 changes: 8 additions & 8 deletions src/byte-queue.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { describe, expect, it } from "vitest";
import { ByteQueue } from "./byte-queue";
import { createByteQueue } from "./byte-queue";
import { readAllBytes, readableFromChunks } from "./index";
import { toU8Array } from "./shared/uint8array";

describe("ByteQueue", () => {
it("reads across chunk boundaries without losing order", () => {
const queue = new ByteQueue();
const queue = createByteQueue();
queue.append(toU8Array([1, 2]));
queue.append(toU8Array([3, 4, 5]));

Expand All @@ -15,7 +15,7 @@ describe("ByteQueue", () => {
});

it("returns the original chunk when reading its full length", () => {
const queue = new ByteQueue();
const queue = createByteQueue();
const chunk = toU8Array([7, 8, 9]);
queue.append(chunk);

Expand All @@ -24,7 +24,7 @@ describe("ByteQueue", () => {
});

it("supports partial reads followed by the remainder", () => {
const queue = new ByteQueue();
const queue = createByteQueue();
queue.append(toU8Array([1, 2, 3, 4]));

expect(queue.read(2)).toEqual(toU8Array([1, 2]));
Expand All @@ -33,14 +33,14 @@ describe("ByteQueue", () => {
});

it("rejects reads larger than the buffered payload", () => {
const queue = new ByteQueue();
const queue = createByteQueue();
queue.append(toU8Array([1]));

expect(() => queue.read(2)).toThrow();
});

it("finds a byte across chunk boundaries without flattening the queue", () => {
const queue = new ByteQueue();
const queue = createByteQueue();
queue.append(toU8Array([1, 2]));
queue.append(toU8Array([3, 0, 4]));

Expand All @@ -49,7 +49,7 @@ describe("ByteQueue", () => {
});

it("discards buffered bytes without allocating a read result", () => {
const queue = new ByteQueue();
const queue = createByteQueue();
queue.append(toU8Array([1, 2]));
queue.append(toU8Array([3, 4, 5]));

Expand All @@ -60,7 +60,7 @@ describe("ByteQueue", () => {
});

it("preserves ordering after repeated reads trigger internal compaction", () => {
const queue = new ByteQueue();
const queue = createByteQueue();
const source = toU8Array(96);

for (let i = 0; i < source.length; i++) {
Expand Down
121 changes: 63 additions & 58 deletions src/byte-queue.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,70 @@
import { throwError } from "./shared/error";
import { toU8Array } from "./shared/uint8array";

export class ByteQueue {
#chunks: Uint8Array[] = [];
#head = 0;
#byteLength = 0;
export interface ByteQueue {
readonly byteLength: number;
append(chunk: Uint8Array): void;
discard(length: number): void;
indexOf(value: number): number;
read(length: number): Uint8Array;
}

get byteLength(): number {
return this.#byteLength;
}
export function createByteQueue(): ByteQueue {
const chunks: Uint8Array[] = [];
let head = 0;
let byteLength = 0;

append(chunk: Uint8Array): void {
if (chunk.byteLength === 0) {
const compact = (): void => {
if (head === 0) return;
if (head === chunks.length) {
chunks.length = 0;
head = 0;
return;
}
if (head >= 32 && head * 2 >= chunks.length) {
chunks.splice(0, head);
head = 0;
}
};

this.#chunks.push(chunk);
this.#byteLength += chunk.byteLength;
}
const append: ByteQueue["append"] = (chunk) => {
const size = chunk.byteLength;
if (size === 0) return;
chunks.push(chunk);
byteLength += size;
};

discard(length: number): void {
if (length < 0) {
throwError("Length must be non-negative");
}
if (length > this.#byteLength) {
throwError("Length exceeds buffered data");
}
if (length === 0) {
return;
}
const discard: ByteQueue["discard"] = (length) => {
if (length < 0) throwError("Length must be non-negative");
if (length > byteLength) throwError("Length exceeds buffered data");
if (length === 0) return;

let remaining = length;

while (remaining > 0) {
const chunk = this.#chunks[this.#head];
const chunk = chunks[head];
if (!chunk) {
throwError("Buffered data is inconsistent");
}

if (chunk.byteLength <= remaining) {
this.#head++;
head++;
remaining -= chunk.byteLength;
continue;
}

this.#chunks[this.#head] = chunk.subarray(remaining);
chunks[head] = chunk.subarray(remaining);
remaining = 0;
}

this.#byteLength -= length;
this.#compact();
}
byteLength -= length;
compact();
};

indexOf(value: number): number {
const indexOf: ByteQueue["indexOf"] = (value) => {
let offset = 0;

for (let index = this.#head; index < this.#chunks.length; index++) {
const chunk = this.#chunks[index];
for (let index = head; index < chunks.length; index++) {
const chunk = chunks[index];
if (!chunk) {
break;
}
Expand All @@ -70,32 +78,32 @@ export class ByteQueue {
}

return -1;
}
};

read(length: number): Uint8Array {
const read: ByteQueue["read"] = (length) => {
if (length < 0) {
throwError("Length must be non-negative");
}
if (length > this.#byteLength) {
if (length > byteLength) {
throwError("Length exceeds buffered data");
}
if (length === 0) {
return toU8Array(0);
}

const firstChunk = this.#chunks[this.#head];
const firstChunk = chunks[head];
if (firstChunk && firstChunk.byteLength === length) {
this.#head++;
this.#byteLength -= length;
this.#compact();
head++;
byteLength -= length;
compact();
return firstChunk;
}

const value = toU8Array(length);
let offset = 0;

while (offset < length) {
const chunk = this.#chunks[this.#head];
const chunk = chunks[head];
if (!chunk) {
throwError("Buffered data is inconsistent");
}
Expand All @@ -105,28 +113,25 @@ export class ByteQueue {
offset += takeLength;

if (takeLength === chunk.byteLength) {
this.#head++;
head++;
continue;
}

this.#chunks[this.#head] = chunk.subarray(takeLength);
chunks[head] = chunk.subarray(takeLength);
}

this.#byteLength -= length;
this.#compact();
byteLength -= length;
compact();
return value;
}

#compact(): void {
if (this.#head === 0) return;
if (this.#head === this.#chunks.length) {
this.#chunks = [];
this.#head = 0;
return;
}
if (this.#head >= 32 && this.#head * 2 >= this.#chunks.length) {
this.#chunks = this.#chunks.slice(this.#head);
this.#head = 0;
}
}
};

return {
get byteLength(): number {
return byteLength;
},
append,
discard,
indexOf,
read,
};
}
4 changes: 2 additions & 2 deletions src/cobs/public.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ByteQueue } from "../byte-queue";
import { createByteQueue } from "../byte-queue";
import { throwError } from "../shared/error";

const COBS_DELIMITER = 0x00;
Expand Down Expand Up @@ -88,7 +88,7 @@ export const createCOBSDecoderStream = (): TransformStream<
Uint8Array,
Uint8Array
> => {
const buffer = new ByteQueue();
const buffer = createByteQueue();

return new TransformStream<Uint8Array, Uint8Array>({
flush() {
Expand Down
6 changes: 3 additions & 3 deletions src/encryption/transformers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ByteQueue } from "../byte-queue";
import { createByteQueue } from "../byte-queue";
import { toArrayBuffer } from "../shared/array-buffer";
import { throwError } from "../shared/error";
import { toU8Array } from "../shared/uint8array";
Expand All @@ -25,7 +25,7 @@ interface EncryptionTransformerOptions {
export class EncryptionTransformer
implements Transformer<Uint8Array, Uint8Array>
{
#pending = new ByteQueue();
#pending = createByteQueue();
#lastRecordCandidate: Uint8Array | undefined;
#sequence = 0;
#recordSize: number;
Expand Down Expand Up @@ -126,7 +126,7 @@ export class EncryptionTransformer
export class DecryptionTransformer
implements Transformer<Uint8Array, Uint8Array>
{
#pending = new ByteQueue();
#pending = createByteQueue();
#lastRecordCandidate: Uint8Array | undefined;
#sequence = 0;
#recordSize: number | undefined;
Expand Down
6 changes: 3 additions & 3 deletions src/png/transformers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ByteQueue } from "../byte-queue";
import { type ByteQueue, createByteQueue } from "../byte-queue";
import { throwError } from "../shared/error";
import { concatU8Arrays } from "../shared/uint8array";
import {
Expand Down Expand Up @@ -140,7 +140,7 @@ export function createPNGTextChunkWriterImpl(
let payloadNotifier = createDeferred<void>();
let outputNotifier = createDeferred<void>();
const outputChunks: Uint8Array[] = [];
const sourceQueue = new ByteQueue();
const sourceQueue = createByteQueue();
let sourcePump: Promise<void> | null = null;
let iendChunkRaw: Uint8Array | null = null;
let outputClosed = false;
Expand Down Expand Up @@ -432,7 +432,7 @@ export function streamPNGTextChunkImpl(
let completed = false;

try {
const sourceQueue = new ByteQueue();
const sourceQueue = createByteQueue();
await readPNGSignature(sourceReader, sourceQueue);

let expectedIndex = 0;
Expand Down