The parsed file header, read once when the reader was constructed:
info (owned metadata -- version, transform, geographical extent, the
attribute columns and which of them are indexed), layout (byte offsets
of the R-tree, the attribute indices and the feature section) and raw
(the FlatBuffers table, for the few fields info does not surface).
Pass it to toCityJSONMetadata for the CityJSON metadata
object.
Lets callers write await using r = await fromFile(path).
The whole file as a CityJSONSeq stream: the metadata line first, then one line per feature, in stored order.
An async generator rather than an array, for the same reason selectAll
is a cursor: a city model does not have to fit in memory, and a caller
writing .jsonl wants to emit each line as it arrives. Callers who do
want it all can Array.fromAsync.
Optionalopts: Int64PolicyReleases the underlying reader if it holds an OS resource -- fromFile
opens a node:fs handle that has to stay open for later queries and so
cannot be closed inside fromFile. Idempotent, and a no-op that
resolves immediately for in-memory sources.
The general query entry point: a spatial filter, paging, or both.
Order of operations, and it matters:
limit, offset, and the query geometry
-- BEFORE touching the reader, so a caller mistake never costs a
request.index_node_size; a hardcoded 16 mis-traverses any file
written with another node size. An attribute query descends one
static B+tree per condition. For a String column the B+tree
answers with CANDIDATES, because its keys are truncated to 50 bytes.postFilterCandidates). This is what turns
candidates into answers, and its position in this list is the whole
point: it runs after the intersection and BEFORE step 4.featuresCount reports the
total MATCH count -- post-filtered, not the candidate count -- and
is unaffected by limit/offset.A where and a spatial given together are AND-intersected on feature
offset: both index searches return their hits sorted ascending and
de-duplicated, so the intersection is a sorted merge. nearest with
where is refused outright (UnsupportedQueryCombination).
The signal is threaded into the actual reads on BOTH paths, not merely
held here: into the R-tree traversal and each hit's feature read when
spatial is given, and into scan's per-feature reads (re-checked
between features) when it is not. A signal that only lived on this
facade would cancel nothing -- the reads are where the in-flight work
is.
Optionalopts: SelectOptionsFcbError with code UnsupportedQueryCombination when
spatial.kind is 'nearest' and a non-empty where is also given;
NoIndex when a spatial query is run against a file that carries no
R-tree; AttributeIndexNotFound when a where names a column that does
not exist or that the writer did not index; UnsupportedColumnType for a
where on a Json or Binary column; InvalidArgument for a negative
or non-integral limit/offset, a malformed query geometry, or a
value whose type does not match its column.
Every feature in the file, in stored order. Async because later
selection modes (spatial, attribute) must read an index before they can
produce their first feature; selectAll has nothing to read, so it
resolves immediately, but the signature is shared.
Takes ReadOpts directly (rather than a SelectOptions-shaped object)
because it has nothing else to validate -- no spatial query, no paging
-- so the only thing worth threading is the signal, straight into
scan's reads.
Optionalopts: ReadOptsStaticfromReads an .fcb file from a Blob -- or a File, which extends it, so
this is the entry point for an <input type="file"> or a drag-and-drop
upload. Backed by Blob.slice(), so only the ranges a query actually
needs are ever materialised. Nothing to close.
StaticfromReads an .fcb file already in memory. The bytes are COPIED (see
BytesRangeReader), so the caller may afterwards mutate the array
or transfer its ArrayBuffer to a worker without corrupting an open
reader. Nothing to close.
StaticfromThe primitive every other constructor and every later feature builds on
(Task 11's fromUrl, Task 12's select, the request-log tests).
The reader is used EXACTLY as given -- no buffering decorator is
inserted here. Caching is a property of the source, not of the facade:
wrapping unconditionally would hide how many requests a scan really
makes, which is precisely what the HTTP work needs to be able to see and
tune. A caller over a chatty transport composes
new BufferedRangeReader(source) itself.
A sequential scan through the resulting cursor issues TWO read calls
per feature, not one: readFeature (src/feature/index.ts) reads the
4-byte size prefix, then re-reads those same 4 bytes as part of a second,
4 + len-byte read for the body. See readFeature's docstring for why.
This reader does nothing to hide that -- a request-count assertion
against reader.reads.length should expect 2 * featuresCount (plus
the header's own reads), not featuresCount. fromFile inherits this
as-is: FileRangeReader has no internal buffering, so a sequential scan
costs two pread syscalls per feature. Callers for whom request count
matters -- HTTP chief among them -- should wrap their source in
new BufferedRangeReader(source) before calling fromReader, exactly as
the paragraph above already says for caching in general.
StaticfromOpens a remote .fcb file over fetch, with strict validation of the
server's Range support (see io/fetch.ts).
Unlike fromReader, this DOES wrap the source in a
BufferedRangeReader -- fromReader's docstring explains why it
itself does not (matching the C++ reference, and leaving request
counting visible to callers who need it), and says a chatty transport
should compose one. HTTP is exactly that transport: without buffering,
the two read() calls readFeature makes per feature (a 4-byte size
prefix, then a 4 + len body read that re-reads those same 4 bytes)
would each become a separate HTTP request.
The buffer starts at OPEN_PREFETCH_SIZE so its first miss -- forced
by readHeader's very first read() call -- asks the underlying
FetchRangeReader for EXACTLY the window that reader already cached
during open() (io/fetch.ts's prefetch), which is why opening
costs one physical request rather than two. Once the header is parsed,
the window is widened to fetchSize (DEFAULT_FETCH_SIZE, 1 MB,
unless overridden) for the feature scan that follows -- mirrors
http_reader/mod.rs's own reset of min_req_size after _open.
Optionalopts: FetchRangeReaderOpts
The reader facade: open an
.fcbresource once, then answer queries against it.Construct one with the static factory that matches the source -- FcbReader.fromUrl (HTTP range requests), FcbReader.fromBlob (a browser
Blob/File), FcbReader.fromBytes (an in-memory buffer), FcbReader.fromReader (any RangeReader) -- orfromFile(path)from the package's separate"./node"subpath, which is where thenode:fssource lives so the package root never importsnode:*. All of them are async because all of them read and validate the header up front.Opening reads the header and nothing else --
fromUrlcosts exactly one HTTP request. FcbReader.select then reads the index it needs and only the features that matched, so a bbox query over a remote file fetches a few index pages plus those features, never the whole file.Call FcbReader.close (or use
await using) when the source holds an OS resource --fromFiledoes; the in-memory,Blobandfetchsources have nothing to release. Either way the reader is spent afterwards: a furtherselectorselectAllthrows.