Deep implementation details of HTTP-layer caching for remote file access: CachedFilelike, VirtualLRUBuffer, connection management algorithm, BrowserHttpReader, FetchReader streaming, and RequestQueue concurrency control.
65%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./.github/skills/remote-caching/SKILL.mdBrowserHttpReader (fetch + Range headers)
│
▼
FetchReader (Streams API → EventEmitter: data/error/end)
│
▼
CachedFilelike (LRU block cache via VirtualLRUBuffer)
│
▼
BatchingReadable (coalesces nearby read() calls within a microtask tick)
│
▼
RemoteFileReadable (IReadable adapter: size(), read(offset, size))
│
▼ ┌─── Worker boundary ───┐
McapIndexedReader (footer → summary → chunk index) │
│ │
▼ │
McapIndexedIterableSource (messageIterator, getBackfillMessages)
│ │
└───────────────────────────────────────────────────────┘
│
▼
BufferedIterableSource (10s read-ahead, 300MB max, producer-consumer)
│
▼
DeserializingIterableSource (lazy deserialization)
│
▼
IterablePlayer (tick loop, state machine)Source: packages/suite-base/src/util/CachedFilelike.ts
Provides in-memory LRU caching for streaming file reads. Sits between BrowserHttpReader (network) and RemoteFileReadable (MCAP reader). Manages a single HTTP connection at a time and intelligently decides when to open new connections.
| Constant | Value | Purpose |
|---|---|---|
CACHE_BLOCK_SIZE | 10 MiB | VirtualLRUBuffer block granularity |
CLOSE_ENOUGH_BYTES_TO_NOT_START_NEW_CONNECTION | 5 MiB | Don't interrupt current download if it's within 5MB of the needed byte |
LOGGING_INTERVAL_IN_BYTES | 300 MiB | Progress log frequency |
Default cacheSizeInBytes (RemoteFileReadable) | 500 MiB | Total in-memory cache budget |
class CachedFilelike {
#fileReader: FileReader; // BrowserHttpReader instance
#cacheSizeInBytes: number; // Max memory (default: Infinity, RemoteFileReadable sets 500MB)
#virtualBuffer: VirtualLRUBuffer; // Block-based LRU memory
#currentConnection?: { stream, remainingRange }; // Single active HTTP stream
#readRequests: { range, resolve, reject }[]; // Pending read queue
#lastResolvedCallbackEnd?: number; // Read-ahead hint
}read(offset, length) → queues a readRequest with range and promise#updateState() fires:
virtualBuffer.hasData())getNewConnection() to decide if a new HTTP stream is needed#setConnection(range):
fileReader.fetch(start, length) → streaming FetchReaderdata chunks: copies into VirtualLRUBuffer, updates remainingRange.start#updateState() to resolve newly-satisfiable readskeepReconnectingCallback: unlimited retries, callback notified of reconnection state#closeWithError() (destroys the active stream, rejects all pending #readRequests, cancels all #activeUncachedReads, resets #virtualBuffer to an empty VirtualLRUBuffer, and closes)#updateState() to retryclose() also funnels through #closeWithError() so explicit close and fatal shutdown share the same cleanup pathif (cacheSizeInBytes >= fileSize) {
// Single block covering entire file (no eviction needed)
new VirtualLRUBuffer({ size: fileSize });
} else {
// Multiple 10MB blocks with LRU eviction
new VirtualLRUBuffer({
size: fileSize,
blockSize: CACHE_BLOCK_SIZE, // 10MB
numberOfBlocks: Math.ceil(cacheSizeInBytes / CACHE_BLOCK_SIZE) + 2,
});
}Source: packages/suite-base/src/util/VirtualLRUBuffer.ts
Represents an entire file in memory using fixed-size blocks, but only keeps numberOfBlocks blocks allocated at any time. Evicts least-recently-used blocks to stay within budget.
byteLength: total file size this buffer represents#blockSize: bytes per block (default ~1GiB, CachedFilelike uses 10MiB)#numberOfBlocks: max concurrent blocks (Infinity = no eviction)#lastAccessedBlockIndices: LRU order array (tail = most recent)#rangesWithData: simplified range array tracking which byte ranges have valid data| Method | Description |
|---|---|
hasData(start, end) | Returns true if entire range is cached (backed by isRangeCoveredByRanges) |
slice(start, end) | Returns Uint8Array — efficient single-block slice or multi-block copy |
copyFrom(source, targetStart) | Writes data, triggers block allocation/eviction |
getRangesWithData() | Returns minimal list of cached ranges (for getNewConnection) |
copyFrom() calls #getBlock(index) for each block the data spans#getBlock(index):
Uint8Array(blockSize)index to end of #lastAccessedBlockIndices (mark as most recently used)#lastAccessedBlockIndices.length > #numberOfBlocks:
shift() the least-recently-used indexdelete #blocks[deleteIndex] (allows GC)#rangesWithData via interval subtractionslice() returns a view (no copy)slice() requires copying into a new bufferintervals-fn library used for range algebra (simplify, unify, substract)Source: packages/suite-base/src/util/getNewConnection.ts
Determines whether CachedFilelike should open a new HTTP connection and what byte range to request. Called every time state changes (data received, read resolved, connection closed).
| Constant | Value | Purpose |
|---|---|---|
READ_AHEAD_BUFFER_SIZE | 50 MiB | How far ahead to proactively download |
1. Compute notDownloadedRanges = missingRanges(readRequest, downloadedRanges)
2. Start new connection if:
a. No current connection exists, OR
b. Current connection doesn't overlap with needed ranges, OR
c. Current connection is >5MB away from first needed byte
3. If cache ≥ fileSize: download from first gap to next downloaded range
4. If downloading to end of request: read-ahead up to 50MB from request start
5. Otherwise: download first missing range1. If cache ≥ fileSize: try to download entire file (prefer after lastResolvedCallbackEnd)
2. If cache < fileSize: download 50MB starting from lastResolvedCallbackEnd
3. Only download ranges not already cached (via missingRanges)The algorithm prioritizes sequential reads — after resolving a read request, it proactively fills the 50MB following that request. This matches MCAP's sequential chunk access pattern during playback.
Source: packages/suite-base/src/util/BrowserHttpReader.ts
open() — File Discoverycache: "no-store" (forces fresh response)Accept-Ranges: bytes header (required for random access)Content-Length for file size{ size, identifier } where identifier is ETag or Last-ModifiedWhy GET instead of HEAD?
Content-Range exposurefetch(offset, length) — Range Requestconst headers = new Headers({ range: `bytes=${offset}-${offset + length - 1}` });
const reader = new FetchReader(url, { headers });
reader.read();
return reader; // FileStream interfaceAccess-Control-Allow-Origin must be setAccess-Control-Expose-Headers must include Accept-RangesRange request headerSource: packages/suite-base/src/util/FetchReader.ts
Wraps the Fetch/Streams API into an EventEmitter pattern (data, error, end) compatible with CachedFilelike's FileStream interface.
class FetchReader extends EventEmitter<{ data, error, end }> {
#response: Promise<Response>; // Queued through globalRequestQueue
#reader?: ReadableStreamDefaultReader<Uint8Array>;
#controller: AbortController; // For cancellation
}read() → getReader() → reader.read() → emit("data", chunk) → read() [recursive]
→ if done: emit("end")
→ on error: emit("error") unless aborteddestroy() sets #aborted = true and calls #controller.abort()"end" (graceful)destroy() when switching connectionsSource: packages/suite-base/src/util/RequestQueue.ts
Global concurrency limiter for HTTP fetch requests. Prevents overwhelming the browser's connection pool or the server.
const GLOBAL_REQUEST_QUEUE_MAX_CONCURRENT = 10; // from constants.ts
export const globalRequestQueue = new RequestQueue(GLOBAL_REQUEST_QUEUE_MAX_CONCURRENT);run(fn): if activeCount < maxConcurrent, executes immediatelyFetchReader construction goes through this queueSource: packages/suite-base/src/players/IterablePlayer/Mcap/RemoteFileReadable.ts
Thin adapter bridging CachedFilelike (byte-offset Filelike API) to McapTypes.IReadable (bigint offset/size API).
const DEFAULT_CACHE_SIZE_BYTES = 1024 * 1024 * 500; // 500MiB
class RemoteFileReadable {
#remoteReader: CachedFilelike; // Cache size configurable, defaults to 500MiB
#batchingReadable: BatchingReadable; // Coalesces reads before CachedFilelike
constructor(url: string, options?: { cacheSizeInBytes?: number; readAheadEnabled?: boolean }) {
const fileReader = new BrowserHttpReader(url);
this.#remoteReader = new CachedFilelike({
fileReader,
cacheSizeInBytes: options?.cacheSizeInBytes ?? DEFAULT_CACHE_SIZE_BYTES,
readAheadEnabled: options?.readAheadEnabled,
});
const inner = {
size: async () => BigInt(this.#remoteReader.size()),
read: async (offset, size) => this.#remoteReader.read(Number(offset), Number(size)),
};
this.#batchingReadable = new BatchingReadable(inner);
}
async size(): Promise<bigint> { return BigInt(this.#remoteReader.size()); }
async read(offset: bigint, size: bigint): Promise<Uint8Array> {
return await this.#batchingReadable.read(offset, size); // → coalesced → CachedFilelike
}
}Source: packages/suite-base/src/players/IterablePlayer/Mcap/BatchingReadable.ts
Coalescing layer between McapIndexedReader and CachedFilelike. Accumulates read() calls that arrive within the same microtask tick, sorts them by offset, and merges those whose gap is < 64 KiB (up to a 4 MiB merged span) into a single underlying read — cutting HTTP Range requests for MCAP files with many small chunks.
McapIndexedReader issues reads strictly sequentially (each awaited before the next), so real coalescing depends on concurrent access — validate request-count reduction empirically for a given workload.Source: packages/suite-base/src/players/IterablePlayer/shared/HydratedSourcePool.ts
Bounds resident heavyweight per-file reader objects (for example McapIndexedReader instances with chunk indexes, channel schemas, and deserializers) using a hybrid count + byte budget. This is a separate layer from CachedFilelike: it manages parsed reader objects, not raw downloaded file bytes and not decoded message payloads.
HydratedSourcePoolOptions = { maxBytes?, maxCount?, minResident? }maxCount is normalized to Math.max(1, Math.floor(...)), or Infinity when unsetmaxBytes defaults to InfinityminResident defaults to 1, then clamps to Math.min(maxCount, Math.max(1, Math.floor(...)))Map<object, Entry> where:
token = caller-owned identity object for one sourceEntry = { hydrator, value: Promise<unknown>, pins: number, weight: number }Map preserves insertion order. Deleting and re-setting an entry on access refreshes recency, so iteration order is LRU order (first entry = least recently used).type SourceHydrator<T> = {
open: () => Promise<T>;
close: (value: T) => Promise<void>;
weigh?: (value: T) => number;
};
type Entry = {
hydrator: SourceHydrator<unknown>;
value: Promise<unknown>;
pins: number;
weight: number;
};
class HydratedSourcePool {
#entries: Map<object, Entry>; // insertion order = LRU order
#totalWeight: number; // sum of resident entry weights
#terminated: boolean;
}| Method | Description |
|---|---|
acquire(token, hydrator) | Returns a resident value, opening it on demand and pinning it while in use |
release(token) | Decrements the pin count (never below 0) and opportunistically triggers eviction |
admit(token, hydrator, value) | Seeds the pool with an already-open value, usually from a source's own initialization path |
terminate() | Prevents future admission/hydration, clears the pool, and closes every resident value |
acquire() / release() lifecycleacquire(token, hydrator) checks #terminated first and immediately throws "HydratedSourcePool has been terminated" when shutdown has started.Map entry to refresh LRU positionpinsvalue promiseopen() does not leak a phantom pinpins: 1, value: hydrator.open(), weight: 0weight = Math.max(0, hydrator.weigh?.(value) ?? 1)#totalWeightpinsthis.#entries.get(token) === entryrelease(token) decrements pins when the entry still exists, then fires-and-forgets #evictBeyondCapacity() so newly unpinned entries can be reclaimed.admit() and terminate()admit(token, hydrator, value) lets callers seed the pool with an already-hydrated value, avoiding a redundant open() call.admit() immediately closes the supplied value instead of retaining it.pins: 0), computes weight, updates #totalWeight, and runs eviction. A newly admitted value may be evicted immediately if the pool is already over budget.terminate() sets #terminated = true before clearing the map so concurrent or later acquire() / admit() calls cannot repopulate the pool during shutdown.#totalWeight to 0, and closes every resolved value in parallel. Each close is individually guarded so one failing close() does not stop the rest.#isOverCapacity() returns:
false when entries.size <= minResidenttrue when either:
entries.size > maxCount, or#totalWeight > maxBytes#evictBeyondCapacity() then:
#isOverCapacity() remains trueMap iteration order (LRU order)pins === 0close() so concurrent eviction passes cannot target it twice#totalWeighthydrator.close(value) and logs errors instead of throwingIf every remaining entry is pinned, eviction stops early. The pool may temporarily remain over maxCount and/or maxBytes; that is intentional because pinned entries are actively in use and cannot be evicted.
#terminated is a hard gate, not just a best-effort hintacquire() after termination throws immediatelyadmit() after termination discards and closes the supplied value immediatelyterminate()estimateReaderWeightBytesSource: packages/suite-base/src/players/IterablePlayer/Mcap/readerWeight.ts
export const READER_BASE_BYTES = 2 * 1024 * 1024; // fixed reader/deserializer overhead
const BYTES_PER_CHUNK_INDEX_BASE = 128; // fixed scalar fields of one ChunkIndex
const BYTES_PER_MESSAGE_INDEX_ENTRY = 64; // one messageIndexOffsets entry per (chunk, channel)
const BYTES_PER_CHANNEL = 16 * 1024; // parsed schema + per-channel deserializer
export function estimateReaderWeightBytes(reader: McapIndexedReader): number {
let messageIndexEntries = 0;
for (const chunkIndex of reader.chunkIndexes) {
messageIndexEntries += chunkIndex.messageIndexOffsets.size;
}
return (
READER_BASE_BYTES +
reader.chunkIndexes.length * BYTES_PER_CHUNK_INDEX_BASE +
messageIndexEntries * BYTES_PER_MESSAGE_INDEX_ENTRY +
reader.channelsById.size * BYTES_PER_CHANNEL
);
}This is the weigh() heuristic for pooled MCAP readers:
READER_BASE_BYTES models fixed parser/deserializer overheadcacheBytes parameter — the weight reflects only reader/index/channel structure size, not each source's byte-cache allocation (for example its CachedFilelike budget)Absolute values are approximate; the relative weighting is what matters. Heavier readers do not get special eviction priority directly — eviction still removes the next LRU unpinned entry — but heavier readers push the pool over maxBytes sooner, causing LRU eviction pressure earlier.
type: "url" persistent transport: pooled indexed URL sources create RemoteFileReadable once, stash it in #persistentReadable, and reuse that same connection + internal CachedFilelike byte cache across every later HydratedSourcePool.acquire() re-hydration; the source hydrator's close() only tears down the heavyweight McapIndexedReader / parsed-channel state. #persistentReadable.close() happens in three cases: indexed initialization itself fails, the source falls back to the unindexed streaming path (raw fetch() bypasses the pool/readable entirely), or the whole McapIterableSource is terminate()d at normal session end (the common case for a healthy indexed source). type: "file" has no analogous persistent transport because the backing Blob is already resident.chunkIndexes.length === 0, channelsById.size === 0, or the URL fallback path triggers), McapIterableSource does not admit() / acquire() it from HydratedSourcePool even when a pool exists; it stores the resulting McapUnindexedIterableSource in #eagerInner for the full session instead, because re-hydrating it would require replaying the whole stream/file from scratch. This still bypasses maxHydratedSources / maxHydratedBytes (those only bound the indexed/pooled path), but it is no longer untracked: MultiIterableSource constructs one EagerUnindexedGuard (shared/EagerUnindexedGuard.ts) per session and hands it to every source via McapSource.eagerUnindexedGuard. On the unindexed branch of initialize(), McapIterableSource calls guard.register(sizeBytes) with the raw file size / Content-Length (a cheap, best-effort proxy — McapUnindexedIterableSource loads the whole file into memory, so encoded size roughly tracks resident footprint), and guard.unregister(sizeBytes) in terminate(). The guard only tracks aggregate count/bytes across the session and logs a single log.warn the first time maxEagerUnindexedSources (default 3) or maxEagerUnindexedBytes (default 2 GiB) is exceeded — it never evicts, blocks, or fails a source open, since that would make an otherwise-playable unindexed MCAP unplayable.When MultiIterableSource handles multiple remote URLs:
const totalCache = dataSource.totalCacheSizeInBytes ?? 500 * 1024 * 1024; // 500MB total
const minPerSource = dataSource.minCachePerSourceBytes ?? 10 * 1024 * 1024; // 10MiB floor
const perSourceCache = Math.max(minPerSource, Math.floor(totalCache / urls.length));
// Each McapIterableSource gets perSourceCache for its RemoteFileReadableExample: 3 remote MCAP files → each gets ~166MB cache budget.
This means:
MIN_CACHE_PER_SOURCE_BYTES = 10 MiB prevents multi-file sessions from slicing the total budget so small that a single MCAP summary/index read can crash CachedFilelikedataSource.minCachePerSourceBytes overrides that floor when a caller needs a different minimumtotalCacheSizeInBytes is not a hard aggregate cap: because perSourceCache = Math.max(minPerSource, Math.floor(totalCache / numSources)), the per-source floor can win when there are many sources. When perSourceCache * numSources > totalCache, a log.warn reports it and the real aggregate cache usage exceeds the nominal totalCacheSizeInBytesreadAheadEnabled still defaults to true for both single- and multi-file sessions (unless dataSource.readAheadEnabled overrides it); what changes for urls.length > 1 is readAheadBufferBytes, which defaults to min(2 MiB, perSourceCache / 4) instead of the legacy 50 MiB default, bounding read-ahead so it doesn't outrun the smaller per-source cache slicetotalCacheSizeInBytes
totalCacheSizeInBytes/perSourceCachegovern the raw byte cache for each source'sCachedFilelike/RemoteFileReadable.HydratedSourcePooladds a separate resident-reader-object budget (maxBytes/maxCount) for parsedMcapIndexedReaderinstances. Both budgets apply at the same time and solve different memory problems.
| File | Role |
|---|---|
packages/suite-base/src/util/CachedFilelike.ts | LRU-cached streaming file reader |
packages/suite-base/src/util/VirtualLRUBuffer.ts | Block-level LRU memory management |
packages/suite-base/src/util/getNewConnection.ts | HTTP connection decision algorithm |
packages/suite-base/src/util/BrowserHttpReader.ts | HTTP Range request implementation |
packages/suite-base/src/util/FetchReader.ts | Streams API EventEmitter adapter |
packages/suite-base/src/util/RequestQueue.ts | Global concurrency limiter (10 max) |
packages/suite-base/src/players/IterablePlayer/Mcap/RemoteFileReadable.ts | IReadable adapter (500MB default); reads pass through BatchingReadable |
packages/suite-base/src/players/IterablePlayer/Mcap/BatchingReadable.ts | Coalesces nearby read() calls (gap <64KiB, ≤4MiB span) into fewer inner reads |
packages/suite-base/src/players/IterablePlayer/shared/HydratedSourcePool.ts | Resident-reader pool with LRU eviction across count and byte budgets |
packages/suite-base/src/players/IterablePlayer/shared/multiFileHydrationOptions.ts | Shared multi-file hydration override merging, used by both data source factories and the MCAP worker |
packages/suite-base/src/players/IterablePlayer/shared/types.ts | SourceHydrator and HydratedSourcePoolOptions type definitions |
packages/suite-base/src/players/IterablePlayer/Mcap/readerWeight.ts | Heuristic weight estimate for pooled MCAP readers |
lichtblick-suite/lichtblick
9589f38
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.