Add cleaning export/API streaming for faster collection access - #30
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three changes so a multi-million-record collection can be read and exported without paging it 1,500 times.
records.ndjson— stream a whole version in one requestNew endpoint:
GET .../versions/:n/records.ndjson. One JSON object per line, ordered by record id, resumable via?after=, filterable by?type=. Paging/recordscosts a round trip per page purely to re-establish a cursor the server just had — 1,556 requests for 3.1M records, against one here.Privacy filtering is identical to
/records.X-Underlay-Record-Countlets a client verify completeness, which matters because a stream that dies partway cannot signal it — the200and headers are long gone.Streaming export
Export previously built each record type as one in-memory array and joined it, which is why it was capped at 250k: at 3.1M the array exhausts the heap and the join would exceed V8's max string length regardless.
It now emits bounded tar parts. Types too large for one entry split into
records/<Type>.0000.ndjson,.0001.ndjson, … at 25k records per part; a type that fits in one part keeps the originalrecords/<Type>.ndjsonname, so existing archives are byte-identical.Cap raised 250k → 2M — now a "don't hand back a multi-GB tarball" guard rather than a memory limit. The SQL explorer keeps its 250k cap, since that one genuinely does hold the version in memory.
Compression
compress()on/api/*, in the app rather than at ingress so local dev matches prod. ~3× on record data.Two bugs found while testing at 3.1M
The stream wasn't streaming. A single unbounded
ORDER BYmade Postgres sort every row before returning the first: ~3.5 GB of temp files, 46s to first byte, andERROR 53100when temp space ran out under concurrent reads. A cursor bounds the client's memory, not the server's — the original comment claiming otherwise was wrong.Replaced with a keyset loop of bounded queries.
LIMITchanges the plan qualitatively, letting Postgres walk(version_id, record_id)in index order with an incremental sort over each small group of equal ids (85 kB peak, no spill). First byte: 46s → 0.31s.Also found
record_idis not unique within a version, so the batch cursor is the(record_id, hash)pair — advancing on id alone drops or repeats rows when a duplicated id straddles a batch boundary.gzip never applied to the stream. Hono's
compress()skips it for two independent reasons:application/x-ndjsonisn't in its compressible-type regex, and it bails onTransfer-Encoding: chunked. The route now compresses itself, via apull-basedReadableStreamso a slow client throttles reads rather than letting batches accumulate.Verification (local, against 3,113,504 records)
Route→ 25000+25000+16316, total 80,490 exactDocs
llms.txt(bulk-read section + when-to-use-which),/protocol(the four guarantees an implementation must honour), versions API reference, integration table, README.Includes the
?after=edge case: because ids aren't unique, a break between two lines sharing an id skips the second on resume — same as/recordspaging, caught by the line-count check.