first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
# sliding-bytes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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
```ts
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`
```ts
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.