20 KiB
enterprise-streams-redesign
Package: @suaveplan/streams
Status: PROPOSED
Type: refactor
Why
The genesis-original enterprise-streams change (archived 2026-04-24) shipped a functional, WHATWG-TransformStream-only API. After auditing ../suaveplan/packages/universal/streams/src/ — the canonical implementation on main — three structural gaps surfaced that block downstream packages (streams-metadata, streams-chunk-queue, future audio-engine, future streams-worker) from being built consistently:
- No plugin contract. Hooks (
crypto:before-chunk,crypto:after-chunk), events (start/chunk/complete/error), parameter introspection, and lifecycle (init/flush/dispose) cannot be expressed in pureTransformStreamfactories. Main'sBaseTransformabstract class — withprocess<T>(input),setOperation,setParams,hook(name, fn),on(name, fn), andtoTransformStream()bridge — is the canonical plugin shape and is required for the DAW-style audio plugin ambition. - No streaming AEAD transform. Main ships
CryptoTransformwith pluggablexchacha20StreamingEngine/aesGcmStreamingEnginethat bind a counter and final-flag into the AEAD AAD, detecting reorder/truncate/tamper. Genesis has no equivalent. Encryption-on-the-fly is one of the explicit driving use cases. - No bidirectional / network primitives. Main ships
BaseDuplexStreamandNetworkStreamImpl(id + protocol + metadata Map) for WebRTC / WebSocket / TCP integration. Genesis has none. These are foundational for the planned drive / network-discovery / audio-network packages.
Additionally, the existing genesis layout uses redundant -stream suffixes on every transform module (transforms/throttle-stream/, transforms/map-stream/, etc.) — confusing inside a package literally named streams. This change normalises naming as part of the redesign.
The 11 existing functional transforms, 3 sources, 3 sinks, fan-out, merge, pipeline-builder, instrument-transform, and node-adapters are all preserved as the implementation backbone — every BaseTransform subclass either subclasses or composes the equivalent functional logic via toTransformStream(), so no behavioural regression is introduced.
What changes
ADDED — plugin substrate (ported from main)
- ADDED:
BaseTransform— abstract class withname,setOperation,setParams,getOperation,getParams,hook(name, fn),removeHook(name, fn),on(name, fn),off(name, fn),process<T>(input, options?)abstract,toTransformStream<TIn, TOut>(): TransformStream<TIn, TOut>bridge to WHATWG,dispose()for cleanup - ADDED:
TransformOptions,IBaseTransform,TransformHookPayload,TransformEventPayloadtypes - ADDED:
StreamManager—pipe(...transforms),execute<T>(input, options?),clear(),getTransforms() - ADDED:
TransformFactory— registry +create(type, ...args)withTransformTypediscriminated union; built-in registrations for every bundled transform - ADDED:
BaseDuplexStream<R, W>primitive withStreamSource<R>andStreamSink<W>configuration - ADDED:
NetworkStreamImplwithid,protocol,metadata: Map<string, unknown>,closedflag,close()andreset() - ADDED: factory functions
createDuplexStream,createNetworkStream,createTransformStream
ADDED — bundled BaseTransform subclasses (port + new)
Ported from main:
- ADDED:
CryptoTransform— pluggable streaming AEAD withengine,key,operation("encrypt" | "decrypt"), hooks (crypto:before-chunk,crypto:after-chunk), events (crypto:start,crypto:chunk,crypto:complete,crypto:error),process(asyncIterable)for stream-of-frames,toTransformStream()for WHATWG bridge. Depends on@suaveplan/cryptostreaming AEAD engines. - ADDED:
SplitJoinTransform— object-mode split withchunkSize+overlap;joinreassembles with optionalseparatorandremoveOverlap; supports string /ArrayBuffer/ array; type guardsisSplittable,isJoinable
Existing 11 functional transforms migrated to BaseTransform subclasses (see § Renames):
- ADDED:
MapTransform,FilterTransform,FlatMapTransform,BatchTransform,WindowTransform,ThrottleTransform,LimitTransform,DedupTransform,TapTransform,TextCodecTransform(encode/decode operations),CompressTransform(compress/decompress operations)
Genesis-original gap-fillers (new):
- ADDED:
ChunkBytesTransform— byte-level fixed-frame re-framing with tail policy (emit|pad|drop) - ADDED:
CoalesceBytesTransform— merges smallUint8Arraychunks until a byte threshold or max-latency timeout - ADDED:
SlidingBytesTransform— overlapping byte windows with size + stride - ADDED:
LengthPrefixEncodeTransform,LengthPrefixDecodeTransform— wire-protocol framing with 1/2/4/8-byte prefix and configurable byte-order
ADDED — pipeline ops (genesis-original)
- ADDED:
partition<T>(source, predicate)→{ truthy, falsy }single-pass split (no tee duplication) - ADDED:
route<T, K extends string>(source, keyFn, keys, options?)→Record<K, ReadableStream<T>>;onUnknownKey: 'drop' | 'error' - ADDED:
routeBy<T, K>(source, keyFn, keys, options?)→Map<K, ReadableStream<T>>(non-string-key variant) - ADDED:
zip<T extends readonly unknown[]>(streams)→ tuple stream withonShortInput: 'close' | 'error' | 'pad'option - ADDED:
concat<T>(streams)→ sequential drain - ADDED:
mergeOrdered<T>(streams, compare)→ k-way merge of pre-sorted streams - ADDED:
safeBranch<TIn, TOut>(factory, options?)→ wraps a sub-pipeline so its errors emit onEventBusLikeinstead of propagating
ADDED — sources, sinks, utilities
- ADDED:
toAsyncIterable<T>(stream, signal?)sink - ADDED:
getDataFromFile(filePath)— Node-only file reader with runtime environment detection (throws in browser) - ADDED:
bufferToUint8Array(buffer)— handles both NodeBufferandArrayBuffer - ADDED:
createUint8ArraySlice(array, start, end)— explicit-typed copy helper - ADDED:
createDataStream<T>(data, transformer, options?)— generic source factory used byStreamManagerfor non-stream inputs - ADDED:
processDataStream<T>(stream, options?)— generic sink factory - ADDED:
streamToUint8Array(stream),uint8ArrayToStream(data, options?),bufferToStream,streamToBuffercross-runtime helpers - ADDED:
validateStreamIntegrity(stream)— chunk-validity sweep helper
MODIFIED — naming convention
Drop redundant -stream suffix on every module name inside the streams package. Implementation file basename matches the parent folder; class and function names follow PascalCase / camelCase conventions without the redundant suffix.
| Old path | New path | Rationale |
|---|---|---|
transforms/map-stream/map-stream.ts |
transforms/map/map.ts |
inside a streams package, "stream" suffix is redundant |
transforms/filter-stream/filter-stream.ts |
transforms/filter/filter.ts |
same |
transforms/flat-map-stream/ |
transforms/flat-map/ |
same |
transforms/batch-stream/ |
transforms/batch/ |
same |
transforms/window-stream/ |
transforms/window/ |
same |
transforms/throttle-stream/ |
transforms/throttle/ |
same |
transforms/limit-stream/ |
transforms/limit/ |
same |
transforms/dedup-stream/ |
transforms/dedup/ |
same |
transforms/tap-stream/ |
transforms/tap/ |
same |
transforms/text-codec/ |
unchanged | already conforms |
transforms/compress/ |
unchanged | already conforms |
pipeline/pipeline-builder/ |
pipeline/builder/ |
redundant package-name prefix |
instrument/instrument-transform/ |
instrument/transform/ |
redundant prefix |
node/node-adapters.ts |
node/adapters/adapters.ts |
promoted to feature dir + redundant prefix dropped |
errors/stream-error.ts |
errors/error/error.ts |
redundant prefix dropped, promoted to feature dir |
The corresponding exported symbols are renamed in lockstep:
| Old export | New export |
|---|---|
mapStream |
mapStream (kept — describes the function shape, not file naming) plus new MapTransform class |
filterStream |
filterStream plus FilterTransform |
flatMapStream |
flatMapStream plus FlatMapTransform |
batchStream |
batchStream plus BatchTransform |
windowStream |
windowStream plus WindowTransform |
throttleStream |
throttleStream plus ThrottleTransform |
limitStream |
limitStream plus LimitTransform |
dedupStream |
dedupStream plus DedupTransform |
tapStream |
tapStream plus TapTransform |
compressStream, decompressStream |
unchanged plus CompressTransform (operation = "compress" | "decompress") |
encodeTextStream, decodeTextStream |
unchanged plus TextCodecTransform |
pipeline() |
unchanged |
fanOut, merge |
unchanged |
instrumentTransform |
unchanged |
fromNodeReadable, toNodeReadable, fromNodeWritable |
unchanged |
StreamPipeFailedError |
renamed to StreamPipeFailedError (lives in errors/error/error.ts now) |
The function-form factories (mapStream, filterStream, …) remain exported for tree-shaking and for direct WHATWG composition; the OO subclasses delegate to them via toTransformStream(). Both call paths are first-class. This is the only "two APIs" pattern this package permits, and it is justified because the OO subclass uses the functional factory under the hood — no parallel implementation, no drift surface.
ADDED — error codes
- ADDED:
STREAM.FRAME_TOO_LARGE,STREAM.TRUNCATED_FRAME,STREAM.ROUTE_UNKNOWN_KEY,STREAM.ZIP_LENGTH_MISMATCH,STREAM.BRANCH_FAILED,STREAM.CRYPTO_DECRYPT_FAILED,STREAM.CRYPTO_INVALID_FRAME,STREAM.DUPLEX_CLOSED,STREAM.NETWORK_RESET,STREAM.FILE_READ_FAILED,STREAM.FILE_BROWSER_NOT_SUPPORTED
REMOVED
- REMOVED: nothing — the previous spec is preserved verbatim under
MODIFIED Requirements. Symbols are renamed (with old paths gone) but exported names are kept on the function-form API.
Out of scope
- Audio-specific anything (sample frames, channel-aware ops, mixing, pitch-shift, dynamics-processor) — belongs in the upcoming
audio-enginepackage as plugins, not in streams. Karaoke/radio transforms from main do not port over. - Image / video / metadata transforms — already split into siblings (
streams-metadataproposed; image/video to follow as separate changes). - Worker offload (
pipeInWorker) — deferred to a futurestreams-workersibling package once audio-engine demands prove the transport shape. schema-registryintegration of stream chunk contracts — deferred until@suaveplan/schema-registryships the consumer hooks the streams chunk format would need.- Mutation testing & contract testing layers — covered by separate
enterprise-testing-mutationandenterprise-testing-contractchanges; this change ships 100% line/branch/function coverage as the baseline.
Source mapping
This change derives from ../suaveplan/enterprise/streams.md (the generic enterprise-quality template applied to streams) and the canonical implementation at ../suaveplan/packages/universal/streams/src/ (the ground-truth API surface).
| Requirement | Source | Notes |
|---|---|---|
| Pure TypeScript, runs in Bun + Node + browser | enterprise/streams.md L38 |
verbatim |
| WHATWG Streams baseline | enterprise/streams.md L40 |
verbatim |
Zero-copy Uint8Array operations on hot paths |
enterprise/streams.md L41 |
verbatim |
| Backpressure-first; bounded queues with declarative high/low watermarks | enterprise/streams.md L42 |
verbatim |
AbortSignal propagates end-to-end |
enterprise/streams.md L43 |
verbatim |
| Zod schemas for stream configuration and chunk contracts | enterprise/streams.md L44 |
partial — schema-registry integration deferred (see Out of scope) |
| Property-based tests via fast-check | enterprise/streams.md L45 |
verbatim |
Deterministic — no Math.random() / Date.now() at module scope, injectable clock + RNG |
enterprise/streams.md L46 |
verbatim |
| OpenTelemetry instrumentation | enterprise/streams.md L47 |
verbatim |
Structured errors extending @suaveplan/error with stable codes |
enterprise/streams.md L48 |
verbatim |
| Idempotency + retry primitives for network-backed adapters | enterprise/streams.md L49 |
applied to NetworkStreamImpl's reset semantics |
| Feature flags for risky transform rollouts | enterprise/streams.md L50 |
applied via FlagRegistry injection on factory; opt-in |
| Plugin / extension interface | enterprise/streams.md L74 |
satisfied by BaseTransform + TransformFactory |
| Observability surface — counters, histograms, spans on every externally-visible op | enterprise/streams.md L75 |
satisfied by instrumentTransform + per-class hook surface |
| Configurability — Zod-validated options with sensible defaults | enterprise/streams.md L76 |
verbatim |
| Performance — benchmark suite + CI regression gate | enterprise/streams.md L77 |
verbatim |
| Resilience — retries / timeouts / cancellation / circuit breakers | enterprise/streams.md L78 |
applied via @suaveplan/retry composition (caller-side) |
| Security — input validation, output encoding, zeroing of sensitive buffers | enterprise/streams.md L79 |
applied to CryptoTransform key zeroize on dispose |
| Migration story for public API changes | enterprise/streams.md L80 |
satisfied by the rename matrix above |
| Example app / story demonstrating full feature set | enterprise/streams.md L82 |
satisfied by examples/ runnable scripts |
Requirements with no enterprise/streams.md line
These derive from ../suaveplan/packages/universal/streams/src/ (canonical implementation on main) and from the enterprise-streams-redesign audit conducted in the brainstorming session.
| Requirement | Source | Justification |
|---|---|---|
BaseTransform abstract class with hook + event surface |
../suaveplan/packages/universal/streams/src/transforms/base/base.ts + base.md |
canonical plugin shape — required for hooks/events that no functional API can express |
StreamManager chainable pipeline |
../suaveplan/packages/universal/streams/src/manager/manager.ts + manager.md |
canonical |
TransformFactory registry |
../suaveplan/packages/universal/streams/src/transforms/factory/factory.ts + factory.md |
canonical |
BaseDuplexStream + NetworkStreamImpl |
../suaveplan/packages/universal/streams/src/primitives/duplex.ts + duplex.md |
canonical |
CryptoTransform with pluggable streaming AEAD |
../suaveplan/packages/universal/streams/src/transforms/crypto/crypto.ts + crypto.md |
canonical; depends on @suaveplan/crypto streaming AEAD engines |
SplitJoinTransform |
../suaveplan/packages/universal/streams/src/transforms/split-join/split-join.ts + split-join.md |
canonical |
Buffer utilities (getDataFromFile, bufferToUint8Array, createUint8ArraySlice) |
../suaveplan/packages/universal/streams/src/utils/buffer/buffer.md |
canonical |
Stream utilities (createDataStream, processDataStream, streamToUint8Array, uint8ArrayToStream, validateStreamIntegrity) |
../suaveplan/packages/universal/streams/src/utils/stream/stream.md |
canonical |
chunkBytes, coalesceBytes, slidingBytes, lengthPrefixEncode/Decode |
genesis audit (this change) | byte-level framing primitives; AEAD requires fixed-byte frames; main's SplitJoinTransform is object-mode only |
partition, route, routeBy |
genesis audit (this change) | single-pass split (fanOut duplicates via tee); essential for "split into different plugin chains by criterion" |
zip, concat, mergeOrdered |
genesis audit (this change) | merge variants beyond arrival-order race; zip enables sample-aligned recombination, mergeOrdered enables k-way time-sorted merge |
safeBranch |
genesis audit (this change) | branch error isolation — without it, one failing FX rack tears down the whole graph |
toAsyncIterable sink |
genesis audit (this change) | async iterator interop, missing from main |
Naming convention: drop redundant -stream suffix |
user direction (brainstorming session) | clarity inside a streams package |
| 100% test coverage threshold | genesis-wide policy (openspec/AGENTS.md § 7) |
non-package-specific |
Source-mapped **Verified by:** lines |
genesis-wide policy (openspec/AGENTS.md § 13) |
non-package-specific |
Verification approach
Every requirement in the spec delta carries a **Verified by:** line pointing at a concrete it(...) title in one of the test files below. Tests are co-located with implementation per the genesis 5-file convention.
src/base/transform/transform.test.ts—BaseTransformlifecycle, hook + event surfaces,toTransformStream()bridge,dispose()semanticssrc/base/transform/transform.spec-conformance.test.ts— type-level: generic narrowing, hook-payload type inference, factory return-type preservationsrc/manager/manager/manager.test.ts— pipeline chaining, sequential execution,clear()reset, error propagationsrc/factory/factory/factory.test.ts— type-discriminatedcreate(), registry lookup, unknown-type errorsrc/primitives/duplex/duplex.test.ts— bidirectional flow, byte vs object mode,cancel()semanticssrc/primitives/network/network.test.ts— id, protocol, metadata,close(),reset()with errorsrc/transforms/<name>/<name>.test.ts— per-transform behaviour (one file per BaseTransform subclass)src/transforms/crypto/crypto.test.ts+crypto.spec-conformance.test.ts— round-trip encrypt/decrypt, hook payloads, event firing order, reorder/truncate detectionsrc/pipeline/<op>/<op>.test.ts— per-op behavioursrc/sinks/to-async-iterable/to-async-iterable.test.ts— async-iterator yields all chunks;signalcancelssrc/utils/buffer/<helper>/<helper>.test.ts— Node + browser branches; env-detection errorsrc/utils/stream/<helper>/<helper>.test.ts— round-trip identity, integrity validationsrc/instrument/transform/transform.test.ts— span + counter + histogram emitted via in-memory OTelMeterProvidersrc/node/adapters/adapters.test.ts— Node ↔ WHATWG bridge round-triptests/integration.test.ts— full pipelines: encrypt → length-prefix → decrypt round-trip; partition + safe-branch + merge compositiontests/property.test.ts—chunkBytesbyte-conservation,partitionorder preservation,zip/unzipround-trip,mergeOrderedsortedness invarianttests/dep-graph.test.ts— only declared workspace deps reachable; nonode:fsimport in browser entrytests/tree-shake.test.ts—sideEffects: falseround-trip viaBun.build
Migration impact
This package has consumers on genesis already — every package that imports from @suaveplan/streams. Audit:
grep -rln "@suaveplan/streams" packages/ --include="package.json"
The function-form API (mapStream, filterStream, pipeline, fanOut, merge, instrumentTransform, fromNodeReadable, toNodeReadable, fromNodeWritable, compressStream, decompressStream, encodeTextStream, decodeTextStream, toArray, toBytes, forEach, fromArray, fromBytes, fromAsyncGenerator) is preserved. Internal subpath imports change:
| Old subpath import | New subpath import |
|---|---|
@suaveplan/streams/transforms/map-stream/map-stream |
@suaveplan/streams/transforms/map/map |
@suaveplan/streams/transforms/filter-stream/filter-stream |
@suaveplan/streams/transforms/filter/filter |
| (… same pattern for the renamed transforms above) | |
@suaveplan/streams/pipeline/pipeline-builder/pipeline-builder |
@suaveplan/streams/pipeline/builder/builder |
@suaveplan/streams/instrument/instrument-transform/instrument-transform |
@suaveplan/streams/instrument/transform/transform |
@suaveplan/streams/node/node-adapters |
@suaveplan/streams/node/adapters/adapters |
@suaveplan/streams/errors/stream-error |
@suaveplan/streams/errors/error/error |
Consumer migration is part of this change. Every internal consumer is updated in the same change folder; no shim or deprecation alias remains. The barrel @suaveplan/streams (root export) is unaffected — top-level imports keep working.
Breaking changes: subpath imports only (top-level barrel preserved). New OO subclasses are pure additions.
Prerequisites: @suaveplan/error, @suaveplan/types, @suaveplan/events, @suaveplan/crypto (for CryptoTransform) must be at their current genesis versions on Verdaccio.