Architecture
fits-js separates what to read from where the bytes come from. The parser and the image decoder ask for byte ranges through one interface, and four adapters supply those ranges from different sources. This page traces how that keeps reads lazy.
The RandomAccessReader interface
Section titled “The RandomAccessReader interface”interface RandomAccessReader { readonly size: number | undefined; read(offset: number, length: number): Promise<Uint8Array>; close?(): Promise<void>;}Everything above the interface is runtime-agnostic. The four implementations:
BytesReaderwraps bytes already in memory.BlobReaderslices a browserBloborFile.HttpRangeReaderturns each read into HTTPRangerequests.NodeFileReaderreads through anode:fs/promisesfile handle. The import is dynamic, so the module loads in browsers even though the class only works wherenode:fsexists.
Opening a file reads only headers
Section titled “Opening a file reads only headers”sequenceDiagram
participant App as Your code
participant Core as openFits
participant Reader as HttpRangeReader
participant Server as HTTP server
App->>Core: openFits(reader)
Core->>Reader: read header blocks (HDU 0)
Reader->>Server: GET Range: bytes=...
Server-->>Reader: 206 Partial Content
Note over Core: Data unit size computed<br/>from NAXIS/BITPIX keywords.<br/>Data bytes never fetched.
Core->>Reader: read header blocks (HDU 1)
Reader->>Server: GET Range: bytes=...
Server-->>Reader: 206 Partial Content
Core-->>App: { hdus, warnings }FITS headers are 2880-byte blocks ending in an END card. For each HDU, openFits reads header blocks until END, computes the data unit’s byte length from the structural keywords (BITPIX, NAXIS, NAXISn), and seeks past it. The data unit is located and measured, never fetched. Malformed structure degrades to entries on the warnings list unless { strict: true } escalates them to throws.
Cutouts fetch only their bytes
Section titled “Cutouts fetch only their bytes”readImage with a region computes which byte ranges inside the data unit the region covers and reads only those. The decoder then applies BZERO/BSCALE scaling, the unsigned-integer convention, and BLANK handling on the fetched bytes. FITS stores big-endian, and decoding produces native-endian typed arrays.
On HttpRangeReader those region reads become a handful of Range requests. The reader caches fetched pages (64 KiB by default, 8 MiB budget) and merges nearby missing ranges into single requests, so row-by-row region reads don’t degrade into one request per row.
What HttpRangeReader guards
Section titled “What HttpRangeReader guards”Remote files can change or be served by misbehaving servers, so the reader is defensive:
- The first read probes with
bytes=0-0. A206answer keeps the range path and captures the total size fromContent-Range. A200answer means the server ignoredRange, and the whole body is read once and served from memory after that. - A validator from the first response (
ETagorLast-Modified) is replayed asIf-Rangeon later requests, so a file that changes mid-read is detected instead of silently mixing two versions. A server that sends no validator cannot be checked this way. - Short
206responses are followed up until the requested range is satisfied. End of file is concluded only from the known size or a416, never from a cache miss.
Package boundaries
Section titled “Package boundaries”@fits-js/core has zero runtime dependencies and stays browser-safe. Capabilities with their own dependency footprint ship as separate packages when they land: @fits-js/arrow for BINTABLE-to-Arrow (depends on apache-arrow), @fits-js/wcs for coordinate transforms, @fits-js/render for display-scaling primitives. The Roadmap has the sequence.