Add streaming bundle reader and incremental JSON parser - #184
Merged
Merged
Conversation
exo-nikita
force-pushed
the
claude/focused-noether-0q8ffx
branch
from
October 1, 2026 05:20
2851b8f to
b8b6213
Compare
exo-nikita
force-pushed
the
claude/focused-noether-0q8ffx
branch
from
October 1, 2026 12:07
80bbd71 to
54ea6ce
Compare
readBundle(source, { onFile, signal }) decompresses and parses a
stasis.code.br chunk by chunk, so neither the decompressed bytes nor the
JSON text are ever held whole. It is opt-in: the stasis commands keep
the one-shot Bundle.parse path.
- Without onFile, it builds the same Bundle as Bundle.parse, through an
incremental JSON parser (json-stream.js) that matches JSON.parse
exactly.
- With onFile, each file's contents go to
`await onFile(file, contents, { signal })`, one at a time in stream
order, keyed by Bundle.fileKeyAt. A symbol placeholder is left in
their place, and Bundle.fromJSON(tree, { contents: false }) validates
the bundle and returns it contents-free. A file delivered twice, or
from outside the bundle's file list, is rejected.
- Sources: a path or file URL, any ArrayBuffer or view, or an (async)
iterable of chunks. Bytes after the brotli stream are ignored, as
brotliDecompressSync ignores them. An abort rejects at once, even
while onFile runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FinLHpaKeRRjstqzMnwj6u
…e orders Comments now keep only what the code doesn't say: why the reader rejects on abort without waiting for onFile, the zlib chunk size, why bytes after the brotli stream are ignored, and the parser's exact-JSON.parse contract. The API itself is described in doc/file-formats.md. Newer bundles write `sources` and `modules` after the metadata, older ones before it. A new test reads the same bundle in both orders, with and without onFile, and the doc says files stream wherever the bundle puts them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FinLHpaKeRRjstqzMnwj6u
exo-nikita
force-pushed
the
claude/focused-noether-0q8ffx
branch
from
October 2, 2026 10:44
54ea6ce to
bbc5d64
Compare
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.
Summary
This PR adds
@exodus/stasis/bundle-reader, a streaming reader for stasis bundles. It decompresses and parses the bundle chunk by chunk, so neither the decompressed bytes nor the JSON text are ever held whole. It is opt-in: the stasis commands keep the one-shotBundle.parsepath. The stasis-core support it builds on,Bundle.fromJSON(…, { contents: false })andBundle.fileKeyAt, landed in #188.Key Changes
JsonStreamParser(stasis/src/json-stream.js): a pure-JavaScript incremental JSON parser over UTF-8 bytes. For any chunking, it gives exactly the result ofJSON.parse(Buffer.concat(chunks).toString('utf8')). That covers the accepted inputs, key order, how a repeated key resolves,__proto__, BOMs and invalid UTF-8. It holds only the value being built and one token in flight. AnonString(value, path)hook lets a caller take a string out of the tree as soon as it completes.readBundle(source, { onFile, signal })(stasis/src/bundle-reader.js):sourceis a path or file URL, anyArrayBufferor view, or an (async) iterable of compressed chunks. Bytes after the end of the brotli stream are ignored, asbrotliDecompressSyncignores them.onFile, it builds the sameBundleasBundle.parse.onFile, each file's contents go toawait onFile(file, contents, { signal })one at a time, in stream order, keyed byBundle.fileKeyAt. A placeholder is left in their place, andBundle.fromJSON(tree, { contents: false })validates the bundle and returns it contents-free. A file delivered twice, or from outside the bundle's file list, is rejected.sourcesandmodulesafter the metadata, older ones before it, and both are supported.onFileis running.stasis/package.jsonexports./bundle-reader.doc/file-formats.md.Tests
tests/json-stream.test.js: differential tests againstJSON.parseunder many chunkings, plus edge cases, UTF-8 handling and seeded fuzzing. A mismatch is re-checked in a fresh process, which works around a V8 key-caching bug in Node 24+.tests/bundle-reader.test.js, covering:Bundle.parse(v1 and v0), andonFileorder and backpressure;onFile, and trailing bytes after the brotli stream;node --run lintis clean andnode --run testpasses all 85 suites.Notable Implementation Details
onFilecalls run one after another, which keeps stream order and applies backpressure to the decompressor.🤖 Generated with Claude Code
https://claude.ai/code/session_01FinLHpaKeRRjstqzMnwj6u