diff --git a/.changeset/lovely-carpets-enjoy.md b/.changeset/lovely-carpets-enjoy.md new file mode 100644 index 0000000..ed5d624 --- /dev/null +++ b/.changeset/lovely-carpets-enjoy.md @@ -0,0 +1,5 @@ +--- +"@hsblabs/web-stream-extras": patch +--- + +refactor: replace ByteQueue class with createByteQueue function for improved instantiation diff --git a/biome.json b/biome.json index c0cabaf..636b165 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,7 @@ }, "files": { "ignoreUnknown": false, - "includes": ["**"] + "includes": ["**", "!.agents", "!.claude"] }, "formatter": { "enabled": true, diff --git a/src/byte-queue.test.ts b/src/byte-queue.test.ts index a932ab9..1b69d81 100644 --- a/src/byte-queue.test.ts +++ b/src/byte-queue.test.ts @@ -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])); @@ -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); @@ -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])); @@ -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])); @@ -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])); @@ -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++) { diff --git a/src/byte-queue.ts b/src/byte-queue.ts index e983cee..fb62ea2 100644 --- a/src/byte-queue.ts +++ b/src/byte-queue.ts @@ -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; } @@ -70,24 +78,24 @@ 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; } @@ -95,7 +103,7 @@ export class ByteQueue { let offset = 0; while (offset < length) { - const chunk = this.#chunks[this.#head]; + const chunk = chunks[head]; if (!chunk) { throwError("Buffered data is inconsistent"); } @@ -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, + }; } diff --git a/src/cobs/public.ts b/src/cobs/public.ts index a3eec21..af6c09f 100644 --- a/src/cobs/public.ts +++ b/src/cobs/public.ts @@ -1,4 +1,4 @@ -import { ByteQueue } from "../byte-queue"; +import { createByteQueue } from "../byte-queue"; import { throwError } from "../shared/error"; const COBS_DELIMITER = 0x00; @@ -88,7 +88,7 @@ export const createCOBSDecoderStream = (): TransformStream< Uint8Array, Uint8Array > => { - const buffer = new ByteQueue(); + const buffer = createByteQueue(); return new TransformStream({ flush() { diff --git a/src/encryption/transformers.ts b/src/encryption/transformers.ts index 3db4fdb..ff4fbd7 100644 --- a/src/encryption/transformers.ts +++ b/src/encryption/transformers.ts @@ -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"; @@ -25,7 +25,7 @@ interface EncryptionTransformerOptions { export class EncryptionTransformer implements Transformer { - #pending = new ByteQueue(); + #pending = createByteQueue(); #lastRecordCandidate: Uint8Array | undefined; #sequence = 0; #recordSize: number; @@ -126,7 +126,7 @@ export class EncryptionTransformer export class DecryptionTransformer implements Transformer { - #pending = new ByteQueue(); + #pending = createByteQueue(); #lastRecordCandidate: Uint8Array | undefined; #sequence = 0; #recordSize: number | undefined; diff --git a/src/png/transformers.ts b/src/png/transformers.ts index 40201a2..13337d8 100644 --- a/src/png/transformers.ts +++ b/src/png/transformers.ts @@ -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 { @@ -140,7 +140,7 @@ export function createPNGTextChunkWriterImpl( let payloadNotifier = createDeferred(); let outputNotifier = createDeferred(); const outputChunks: Uint8Array[] = []; - const sourceQueue = new ByteQueue(); + const sourceQueue = createByteQueue(); let sourcePump: Promise | null = null; let iendChunkRaw: Uint8Array | null = null; let outputClosed = false; @@ -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;