Files
skills/docs/foundation/flow/streams/src/transforms/sliding-bytes/sliding-bytes.md
2026-05-29 08:20:56 +01:00

6.3 KiB

sliding-bytes

(no submodules)

Purpose

The sliding-bytes transform re-frames a Uint8Array byte stream into fixed-length overlapping (or gapped) byte windows, advancing by a caller-controlled stride per emission. It is the byte-oriented counterpart of the generic window transform: where window operates on item streams and emits T[] arrays, slidingBytes operates directly on bytes and emits zero-copy Uint8Array windows of length size.

This is the right primitive when you need fixed-size frames with overlap semantics — for example, computing rolling hashes (Rabin-Karp, content-defined chunking previews), running a sliding FFT or convolution, batching audio samples for a vocoder, or feeding a signature-detection state machine that must straddle frame boundaries. Unlike chunkBytes, sliding windows can revisit the same byte across multiple emissions, which is load-bearing for any algorithm whose context window exceeds its hop size.

Features

  • Functional factory slidingBytes(size, stride) returning a TransformStream<Uint8Array, Uint8Array>.
  • Plugin-OO wrapper SlidingBytesTransform extending BaseTransform with hook/event/dispose semantics inherited from the base class.
  • Configurable size (window length) and stride (emission step).
  • stride < size produces overlapping windows (overlap = size - stride).
  • stride === size produces non-overlapping windows (equivalent to a drop-tail chunkBytes).
  • stride > size produces gapped windows that discard stride - size bytes between consecutive emissions.
  • Carry buffer that correctly accumulates and re-emits across upstream chunk boundaries, regardless of how the producer happens to slice its writes.
  • Trailing residual smaller than size is dropped on flush — sliding windows are fixed-length by definition.
  • Synchronous RangeError for invalid size or stride (<= 0, non-finite).

Usage: Basic Example

import { fromBytes } from "@suaveplan/streams/readable/from-bytes/from-bytes";
import { toArray } from "@suaveplan/streams/sinks/to-array/to-array";
import { slidingBytes } from "@suaveplan/streams/transforms/sliding-bytes/sliding-bytes";

const input = new Uint8Array(16);
for (let i = 0; i < 16; i++) input[i] = i;

const windows = await toArray(
    fromBytes(input, 16).pipeThrough(slidingBytes(8, 4))
);
// windows.length === 3
// windows[0] = bytes 0..7
// windows[1] = bytes 4..11   (overlaps windows[0] by 4 bytes)
// windows[2] = bytes 8..15   (overlaps windows[1] by 4 bytes)

Usage: Advanced Example

import { SlidingBytesTransform } from "@suaveplan/streams/transforms/sliding-bytes/sliding-bytes";

// Gapped sampling: read 4 bytes, skip 4, read 4, skip 4, ...
const sampler = new SlidingBytesTransform({ size: 4, stride: 8 });

// Compose with the rest of the pipeline via the standard plugin surface.
const stream = sampler.toTransformStream<Uint8Array, Uint8Array>();

// Plug into any ReadableStream<Uint8Array> source. The transform is stateful
// across chunk boundaries — the upstream is free to deliver bytes in any
// chunk granularity (1 byte at a time, mega-chunks, anything in between)
// and the windowing semantics are identical.
const out: Uint8Array[] = [];
await stream.readable.pipeTo(
    new WritableStream({
        write(chunk) {
            out.push(chunk);
        },
    })
);

API Reference

slidingBytes(size: number, stride: number): TransformStream<Uint8Array, Uint8Array>

Returns a WHATWG TransformStream that emits Uint8Array windows of length size, advancing by stride bytes per emission.

  • size — window length in bytes. Must be a finite number > 0.
  • stride — emission step in bytes. Must be a finite number > 0.

Throws RangeError synchronously when either argument is non-finite or <= 0. Errors the stream with TypeError when an upstream chunk is not a Uint8Array.

class SlidingBytesTransform extends BaseTransform

Plugin-OO wrapper. Constructor takes a SlidingBytesTransformConfig with size and stride fields. Same validation as the functional factory.

  • name — "sliding-bytes".
  • process<T>(input: T): Promise<T> — degenerate single-shot: returns input unchanged. Real windowing requires stream-mode use because the operator is stateful across chunks.
  • toTransformStream<A, B>(): TransformStream<A, B> — delegates to slidingBytes(size, stride). Each call returns a fresh stream.

interface SlidingBytesTransformConfig

interface SlidingBytesTransformConfig {
    readonly size: number;
    readonly stride: number;
}

Implementation Notes

  • Carry buffer. Each upstream chunk is concatenated onto a residual carry buffer. The transform then emits every complete size-byte window starting at offsets 0, stride, 2*stride, ... within that combined buffer. Once no further window fits, the bytes from the next would-be start offset onward are retained as the new carry. When stride > size and the offset overshoots the buffer (the "gap" case), the carry is reset to empty so the inter-window bytes are correctly discarded.
  • Zero-copy emission. Each emitted window is copied into a fresh Uint8Array rather than a subarray view. This is required because the carry buffer is replaced on every transform call and a subarray would alias memory whose lifetime ends at the next chunk. The cost is one size-byte copy per emission; the win is no aliasing-induced memory pin.
  • Flush policy. The trailing residual that cannot fill a complete window is dropped. Unlike chunkBytes (which exposes tail: emit | pad | drop), sliding windows are only meaningful at exact length, so there is no configurable tail policy here. Callers needing partial-tail behaviour should compose with chunkBytes instead.
  • Single-shot degeneracy. SlidingBytesTransform.process(input) returns the input unchanged. The operator is fundamentally a streaming construct — a single buffer has only one "frame," and the most useful semantics for a one-call API is the identity. Use toTransformStream() for real work.
  • Validation. Both the factory and the class throw RangeError for non-finite or non-positive size/stride. Validation runs synchronously at construction so misuse fails before any data flows.