first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user