12 KiB
Add byte-range reads to Drive.createReadStream and drive*:// URI plugins
Packages: @suaveplan/drive, @suaveplan/drive-uri
Status: PROPOSED
Type: capability-extension
Defers from: openspec/archive/2026-05-16-add-drive-uri/proposal.md § "Out of scope → Range reads on drive+mesh://"
- Design: ./design.md
- Spec deltas:
- ./specs/drive/spec.md
- ./specs/drive-uri/spec.md
Why
add-drive-uri shipped resolver.read("drive+mesh://swarm-x/clip.mov") as a whole-file fetch only, explicitly carving out byte-range reads:
Range reads on
drive+mesh://.drive.createReadStreamalready streams chunk-by-chunk via the chunks plugin; byte-range slicing on top requires a futureDrive.createReadStream({ offset, length })extension — tracked separately.
Range reads are the single most-asked-for missing capability for the drive surface — every consumer that wants HTTP Range: header support (audio/video seek, virtual scrolling over chunked logs, partial-image decode, resume-on-reconnect, S3-style multipart staging) is currently forced to fetch the full file and slice client-side. Over drive+mesh:// this is catastrophic: a 4 GiB media file pulled to seek the last 1 MiB downloads the entire swarm payload. The chunked layout in drive-plugin-chunks already addresses every chunk by (fileId, chunkId) with a known fixed chunkSize per manifest, so range → chunk math is deterministic; the missing piece is plumbing the offset/length pair from Drive.createReadStream through the chunks-plugin middleware so only the chunks intersecting the range are fetched, decrypted, and emitted.
This change adds the { offset, length } options to Drive.createReadStream, extends the chunks plugin's #reassemble to honour them (computing first/last chunk indices, slicing the boundary chunks), threads range hints through the drive-uri adapters, and — over drive+mesh:// — emits a ChunkRequest.specific (or bitset for large contiguous ranges) listing only the in-range chunkIds rather than ChunkRequest.whole. The local-storage and template paths get the same offset/length math for free because they go through the same chunks-plugin middleware.
What changes
@suaveplan/drive
- MODIFIED:
Drive.createReadStream(path, signal?)→Drive.createReadStream(path, options?: RangeReadOptions | AbortSignal). The new positional-options overload accepts{ offset?: number; length?: number; signal?: AbortSignal }. The bare-AbortSignalform is preserved via overload so all existing call sites compile unchanged. - ADDED:
RangeReadOptionsinterface insrc/types/drive.types.ts—offsetdefaults to0,lengthdefaults to "to EOF". Both MUST be non-negative integers when present; violations raiseDRIVE.RANGE_INVALIDsynchronously (before stream construction). - ADDED:
DRIVE.RANGE_INVALIDandDRIVE.RANGE_OUT_OF_BOUNDScodes inDRIVE_CODES.RANGE_INVALIDcovers negative / non-integer / NaN inputs.RANGE_OUT_OF_BOUNDScoversoffset > sizeandoffset + length > sizewheresizeis known at stream-start (e.g. via the chunks-plugin manifest); when size is unknown at start-time, the stream end-event MUST surfaceRANGE_OUT_OF_BOUNDSif the underlying source closed beforelengthbytes were emitted. - MODIFIED:
defaultCreateReadStreamand the storage-capability passthrough insrc/file-operations/streams.tshonourRangeReadOptions. The default buffered path slices the loadedUint8Array; the native-streams path forwards{ offset, length }tostorage.createReadStreamwhenstorage.capabilities?.nativeRange === true, otherwise wraps the native stream in a slicing transform that drops bytes beforeoffsetand stops afterlength. - ADDED:
DriveStoragecapability flagcapabilities.nativeRange?: booleanand an extendedcreateReadStream(path, options?: { offset?: number; length?: number; signal?: AbortSignal })signature on the storage layer. Storages that already accept a range (e.g. file withfs.createReadStream({ start, end })) opt in by setting the flag; existing storages are unaffected. - ADDED: Middleware context carries
range?: { offset: number; length?: number }. The chunks-plugin middleware reads it and short-circuits its#reassembleto the intersecting chunk window.
@suaveplan/drive-plugin-chunks
- MODIFIED:
ChunksPluginHandle.#reassemble(fileId, range?)accepts an optional range and emits only the chunks intersecting[offset, offset + length). First and last chunks are sliced; interior chunks are emitted whole. Algorithm indesign.md§ "Range → chunk math". - MODIFIED: The
readmiddleware forwardsctx.range(when present) to#reassembleand to a sibling#reassembleAsStreamused bycreateReadStream. The middleware'sread(path)buffered call buffers only the in-range bytes. - ADDED:
CHUNKS.RANGE_OUT_OF_BOUNDScode — raised when the requested range exceedsmanifest.fileSize. Surfaced on streamerror.
@suaveplan/drive-mesh-protocol
- MODIFIED:
drive-mesh's pull path (consumer of this package; no protocol wire change) constructsChunkRequest.specific { chunkIds }for ranges spanning ≤ 64 chunks andChunkRequest.bitset { bytes }for larger contiguous spans. No new wire fields — both forms already exist inChunkRequestPayload. - No
DMP_*registry changes.
@suaveplan/drive-uri
- MODIFIED:
DriveStorageLikeAdapter.createReadStream(key, options?)accepts and forwards{ offset?, length?, signal? }. The three plugins (driveUriPlugin,driveMeshUriPlugin,driveTemplateUriPlugin) advertisecapabilities.nativeRange = trueon their adapter soStorageUriResolverissues a single range-awarecreateReadStreamcall instead of a whole-fileget+ client-side slice. - ADDED:
DRIVE_URI.RANGE_UNSUPPORTEDis not introduced — everydrive*://URI now supports ranges because the underlying chunks plugin does. The resolver surfaces upstreamDRIVE.RANGE_INVALID/DRIVE.RANGE_OUT_OF_BOUNDSdirectly under the existing pass-through rule. - ADDED: OTel attribute
drive.uri.range = "{offset}-{end}"oncreateReadStreamspans (omitted when no range supplied). Histogramdrive_uri.range_chunks_fetchedtags{scheme}and records the count of chunks actually pulled — measurable savings vs. whole-file fetch.
Out of scope
- Multi-range reads (RFC 7233
multipart/byteranges). One range per call. A caller wanting N disjoint ranges issues NcreateReadStreamcalls. Drive.read(path, { offset, length })buffered range read. Achievable today viacreateReadStream({ offset, length })+ buffer-collect; adding it to the buffered path would duplicate the chunk-math without changing observable behaviour. Hoist if a real consumer needs it.- Range writes / partial uploads. Writes remain whole-file. Chunk-level patch semantics belong in a future
add-drive-patchchange. - Range hints on
drive-tierread()for non-chunked storages withoutnativeRange. Falls back to the slicing transform; performance characteristics are documented but no new code path is added. - Backpressure-aware peer scheduling for large bitset requests. The mesh plugin's existing
ParallelPullerwindow applies unchanged; range simply narrows the chunk set it pulls from.
Source mapping
This is a genesis-original extension; deferral source is openspec/archive/2026-05-16-add-drive-uri/proposal.md. Surface anchors:
Drive.createReadStream—packages/drive/drive/src/drive/drive.ts:222-230,packages/drive/drive/src/file-operations/streams.ts.- Chunks plugin
#reassemble—packages/drive/drive-plugin-chunks/src/plugin.ts:218-251; chunkSize lives on the manifest (packages/drive/drive-plugin-chunks/src/manifest/build.ts:11,30,74), default 65536 (packages/drive/drive-plugin-chunks/src/plugin.ts:78). ChunkRequestunion —packages/drive/drive-mesh-protocol/src/types/protocol.types.ts:38-42; bothspecificandbitsetalready encodable.drive-uriadapter —packages/drive/drive-uri/src/plugins/drive-mesh/drive-mesh.tsextendsInstrumentedDriveAdapter.
Requirements with no source line
| Requirement | Justification |
|---|---|
Overload createReadStream(path, signal) AND createReadStream(path, options) rather than breaking the single-signal form |
Every existing consumer in the monorepo (chunks plugin, drive-uri adapters, drive integration tests) passes the bare AbortSignal. A breaking signature change for an additive feature is unjustified when overloads cost nothing. |
Switch ChunkRequest.specific ↔ ChunkRequest.bitset at 64 chunks |
MAX_CHUNKS_DEFAULT = 64 in drive-mesh-protocol; beyond that, the JSON chunkIds array out-weighs a bitset that addresses every chunk in two bytes per 16. The 64-chunk pivot matches the wire-cap and minimises bytes-on-wire for the common case (4 MiB range over 64 KiB chunks). |
RANGE_OUT_OF_BOUNDS may fire at start OR at stream-end |
When size is known (chunks-plugin manifest loaded, storage stat populated), reject synchronously. When unknown (raw defaultCreateReadStream over a non-stat-able storage), accept the range, emit what's available, then surface the code on close if shortfall is detected. Two paths because the size-known/size-unknown split is a real storage capability variance, not a defect. |
| Zero-length range is a valid request | length: 0 returns an immediately-closed empty stream. Useful as a probe-for-existence pattern; rejecting it would surprise consumers who compute ranges from upstream API responses. |
nativeRange storage capability flag rather than always-slicing in drive |
bun:sqlite-backed and fs-backed storages can deliver true range reads with no read-amplification. Always wrapping in a slicing transform would discard that win for the storages that have it. The flag is opt-in so adding it is purely additive on every existing storage. |
Migration
This is a purely additive change for every existing call site:
- Old:
drive.createReadStream("file.bin", abortSignal)→ still compiles, still returns whole-file stream. - New:
drive.createReadStream("file.bin", { offset: 1024, length: 4096, signal: abortSignal })→ emits exactly bytes [1024, 5120). Overdrive+mesh://, requests at most ⌈(4096 + 1024 % chunkSize) / chunkSize⌉ chunks from peers.
No consumer must change. Consumers that want range behaviour migrate at their leisure. Search command for opt-in adoption:
grep -rn "createReadStream" packages/ | grep -v "/dist/\|/coverage/"
Recommended replacement when range-aware: pass { offset, length, signal } instead of the bare signal.
Impact
Affected dependents
Run bunx turbo ls --filter=...@suaveplan/drive to enumerate. The blast radius is "every package that calls Drive.createReadStream," which today is:
@suaveplan/drive-uri— [x] tests run / [x] docs reviewed / [ ] no action needed (adaptercreateReadStreamextended to forward range; new requirement added to spec delta)@suaveplan/drive-plugin-chunks— [x] tests run / [x] docs reviewed / [ ] no action needed (middleware honours range;#reassembleextended)@suaveplan/drive-mesh— [x] tests run / [x] docs reviewed / [ ] no action needed (pull path constructsChunkRequest.specific/bitsetfrom range)@suaveplan/drive-transport-https— [ ] tests run / [x] docs reviewed / [x] no action needed (transport carries chunk envelopes opaquely; no surface change)@suaveplan/drive-transport-webrtc— [ ] tests run / [x] docs reviewed / [x] no action needed (same as above)@suaveplan/drive-template— [ ] tests run / [x] docs reviewed / [x] no action needed (hydration unchanged; range applies post-hydration)@suaveplan/drive-sharing— [ ] tests run / [x] docs reviewed / [x] no action needed (callsDrive.read, notcreateReadStream)@suaveplan/drive-webrtc— [ ] tests run / [x] docs reviewed / [x] no action needed (browser-tier transport, unaffected)
Affected upstreams (none modified)
| Package | Action | Breaking? |
|---|---|---|
@suaveplan/storage |
Optional: extend AbstractStorage.createReadStream with { offset, length } options. Existing storages opt in via capabilities.nativeRange. |
no |
@suaveplan/storage-uri |
None — already routes createReadStream through UriPlugin.adapter. |
no |
@suaveplan/types |
None — RangeReadOptions lives in @suaveplan/drive until a second consumer emerges. |
no |
Ordering
Unblocked. All packages exist on genesis today; the change is additive.