All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Added
writeEvent()toWriter,WriterSync, andWriterSyncSink, enabling event-reader → transform → writer pipelines for every standard StAX event represented byAnyXmlEvent. - Added XML declaration metadata (
version,encoding, andstandalone) toSTART_DOCUMENTevents and empty-element fidelity (selfClosing) toSTART_ELEMENTevents. - Added DTD output through
writeDTD()and DTD event forwarding throughwriteEvent().
- Start-element events now retain namespace declarations in their ordered
EventAttributes, allowing namespace-aware reader/writer round trips. Elements without attributes exposeattributesasundefined. - Materialized events keep a stable runtime property layout, using
undefinedfor absent optional values to preserve predictable hidden classes. - Async readers may consume source bytes on the first
next()before returningSTART_DOCUMENT, because BOM, XML declaration, and DTD preamble parsing must precede that event. - Sync and async writers now share one serializer state machine through
WriterCore, aligning namespace, validation, declaration, DTD, and self-closing behavior across output targets. - Expanded compiled converter coverage across supported schema, writer, event input, XPath dispatch, optional/default, and transform combinations.
- DTD declarations are surfaced and can be written, but are not automatically
applied and external entities are never resolved. Applications can explicitly
install reviewed internal replacements with
addEntities; parsing a DTD never triggers filesystem or network access.
- Raised the maintained unit-test coverage gate to 100% for statements, branches, functions, and lines, excluding only documented unreachable paths.
- Removed per-call chunk-array draining from
WriterSync/WriterSyncSinkand coalesced each async serializer operation before buffering. This preserves the sharedWriterCorestate machine while restoring writer throughput close to the v1.0 implementation.
- Finalized the pure JavaScript package contract without native addon or Wasm parser runtime dependencies.
- Reduced public package entrypoints to
stax-xmlandstax-xml/converter. The rc3 adapter, tree/object helper, and retired cursor subpaths are not part of the 1.0 API; migrate toStreamReaderSync/StreamReader, event readers, or the converter. - Centered the release documentation on
StreamReaderSync,EventReader, converter, and writer surfaces. - Split release benchmark table data by release version so archived rc3 docs continue to render rc3 data while the latest docs render the v1.0.0 snapshot.
- Refreshed the release benchmark workflow for the final v1.0.0 snapshot,
including runtime matrix, 4 GiB
StreamReaderSync, converter compiled batch-plan, and 1 GiB writer evidence.
- Added release-readiness, v0.x migration, and web-server integration guides so release consumers can verify the pure JavaScript package contract, move from older APIs, and keep request bodies streaming in common server frameworks.
- Re-centered the package on the pure JavaScript implementation: no native addon, Wasm parser module, or backend-selection mode is part of the public package contract.
- Reworked the prerelease validator into a pure JavaScript package gate that checks manifests, exports, workspace layout, source references, and packed npm contents for native-addon or Wasm parser artifacts.
- Updated the release documentation around
StreamReaderSync,EventReader, converter, writer, runtime behavior, and XPath conformance for the rc3 surface. - Replaced hardcoded benchmark tables in the docs with generated tables that read the rc3 benchmark JSON snapshot.
- Refreshed the release benchmark set on Node 24.15.0, Bun 1.3.13, and Deno
2.7.13 with 16 MiB runtime rows, a 4 GiB
StreamReaderSyncindex-first row, converter compiled batch-plan rows, and the 1 GiB writer artifact. - Optimized converter compiled batch dispatch for the pure JavaScript path while keeping converter positioned as a convenience/schema wrapper over the lower level stream reader surface.
- Removed native-addon and Wasm-parser positioning from release-facing docs and benchmarks. Historical implementation notes are kept out of the public docs.
A new cursor-based XML reader API that provides a mutable singleton cursor instead of creating event objects per node. Ideal for high-throughput and memory-constrained environments.
StaxXmlCursorReader— Sync cursor for in-memory XML stringsStaxXmlCursorReaderAsync— Async cursor forReadableStream(Web Standard), chunk-based parsing for multi-GB filesCursorEventType— SMI integer constants (0–6) for cursor event types
Import from stax-xml/cursor:
import { StaxXmlCursorReader, StaxXmlCursorReaderAsync, CursorEventType } from 'stax-xml/cursor';Design Principles:
- All mutable cursor fields are V8 SMI (Small Integer) to bypass write barriers
- Sync cursor uses absolute positions (JS string max ≪ SMI max)
- Async cursor uses relative offsets from a
_baseanchor, enabling multi-GB stream parsing - Position-based element stack eliminates string allocations during traversal
- Lazy attribute parsing defers work until
getAttribute*()is called - Namespace fast-path (
_nsActiveflag) skips namespace resolution for non-namespaced XML
Benchmark Results (vs event parser):
| Size | Iterate (cursor) | Selective (cursor) | Consume (cursor) | GC Memory |
|---|---|---|---|---|
| 2KB | 10% faster | 5% faster | 11% slower | 10 KB |
| 4KB | 12% faster | ~parity | 24% slower | 10 KB |
| 13MB | 40% faster | 34% faster | 24% faster | 78 KB |
| 98MB | 47% faster | 38% faster | 30% faster | 80 KB |
Cursor excels at large files where reduced GC pressure and skipped work compound. Small-file consume is slower due to per-getter string slicing overhead vs parser's V8-optimized young-gen scavenger.
- Added
StaxXmlWriterSyncSinkfor incremental synchronous XML writing without building the full XML string in memory. - Added platform adapter subpaths:
stax-xml/adapters/nodewithcreateNodeSyncTextSink()andcreateNodeFileSyncTextSink()stax-xml/adapters/bunwithcreateBunSyncTextSink()stax-xml/adapters/denowithcreateDenoSyncTextSink()
writeSync()can now accept an injectedStaxXmlWriterSyncSinkthroughWriteOptions.writer.
- Updated README and docs for the cursor API, sync writer sinks, and converter writer injection.
- Refreshed canonical release benchmark artifacts and docs benchmark pages from local benchmark output.
- Added a writer benchmark case for
StaxXmlWriterSyncSinkwith an in-memory file-like target. - Split performance tests out of the unit-test matrix; performance validation remains a manual benchmark workflow.
- Reworked converter API
.compile()so supported schemas lower to a true dispatch-based compiled path instead of the previous matcher/state-machine execution path. - Static XPath selectors are analyzed at compile time and executed as fixed XML event dispatch during parsing. This covers absolute selectors, simple descendant selectors, relative selectors inside object or array items, attributes,
text(), nested objects, scalar arrays, object arrays, optional fields, and transforms. - Unsupported compiled shapes now fall back directly to the normal runtime converter path, preserving compatibility without pretending to use the fast path.
- The
converter-plain-outputbenchmark now shows the compiled converter path running close to the handwritten parser path while remaining declarative.
- Tightened sync sink close/flush semantics and added regression coverage.
- Improved converter, cursor, parser, and writer branch coverage. The package test suite now reports 100% branch coverage.
StaxXmlParser.nextBatch()andbatchedIterator()now operate on chunk-derived batches instead of caller-sized batches.- Async parsing no longer yields on every event while buffered events are still available; chunk boundaries are now the primary async suspension points.
- Nested converter parsing paths reuse buffered async batches instead of repeatedly calling
await iterator.next().
- Compiled converter execution continues to use a single root processor while reducing async overhead in nested parsing paths.
- Bare
x.array(...).compile()and root-processor async cases were rerun against the new batch-backed iterator behavior.
The async XML parser has been completely rewritten with a new fast-path architecture, replacing the previous generator-based implementation. StaxXmlParserFastPathExperimental is now the main StaxXmlParser (the old name remains as a backward-compatible alias).
Key optimizations applied:
-
Sync Fast-Path in Custom Async Iterator —
next()returns a plain{ value, done }object synchronously when buffered events are available, bypassing the microtask queue. APromiseis only created when a new chunk is needed from the stream. This eliminates thousands of Promise allocations per document. -
Single-string Pending Tail — Replaced
pendingStructuralSegments: string[]with a singlependingTail: string, removing array allocation overhead at chunk boundaries. -
Circular Buffer Queue (O(1) dequeue) — Event queue uses a circular buffer with head/tail pointers instead of
Array.shift(), eliminating O(n) dequeue cost. -
Simple-Element Fast Path — Elements with no namespace prefix and no attributes (≈65% of elements in typical XML) bypass attribute parsing entirely and share the parent namespace map (no
new Map()copy). -
Lazy Namespace Map Copy —
new Map(parentNamespaces)is deferred until the firstxmlnsattribute is encountered, avoiding allocations for non-namespace elements. -
xmlns Pre-filter — Attribute namespace checks gate on
charCodeAt(0) === 120(x) before any string comparison, skipping the check for non-xmlns attributes in a single instruction. -
Native
string.indexOf()for tag scanning — Replaced manualcharCodeAtloops withstring.indexOf('<', pos)andstring.indexOf('>', pos), leveraging V8's SIMD-optimized native search. -
Whitespace check without
trim()—flushTextSegmentschecks whitespace viacharCodeAtloop instead of allocating a trimmed copy.
Benchmark Results (midsize.xml — 13MB):
stax-xmlasync (experimental → main): 309ms vs published v0.5.2 516ms (−40%)stax-xmlasync vs txml: 309ms vs 516ms (txml comparison on same dataset)stax-xmlsync consume: 108ms vs txml 156ms (−31%)
The sync parser received the same hot-path optimizations: native indexOf for scanning, startsWith for pattern matching, simple-element fast path, lazy namespace copy, and xmlns pre-filter.
StaxXmlParsernow uses the new fast-path implementationStaxXmlParserFastPathExperimentalremains exported as a backward-compatible alias forStaxXmlParsercreateStaxXmlParser()factory function added (mirrors existingcreateStaxXmlParserFastPathExperimental)- The package now publishes ESM-only artifacts; CommonJS entrypoints were removed before the first
0.6.0npm release - All 800 unit tests pass
The XML Writer has been significantly optimized through three key algorithmic improvements:
-
Regex Caching (+9.3%)
- Static readonly patterns for basic XML entities
- Construction-time compilation for custom entities
- Eliminates repeated regex creation overhead
-
Attribute String Batching (+36.5%)
- Single string concatenation before write operation
- Reduces function call overhead from 4 calls to 1 per attribute
- Exceptional improvement in attribute-heavy documents
-
Early Entity Check (+25.6%)
- Fast string.includes() checks before regex operations
- Avoids expensive regex when no entities are present
- Significant performance gain for clean text content
Benchmark Results (50,000 elements):
- Realistic documents: +18.4% (180ms → 147ms)
- Mixed content: +54.1% (99ms → 46ms)
- Deep nested structures: +63.8% (86ms → 31ms)
- Attribute-heavy: +35.8%
All optimizations maintain 100% API compatibility and pass all 796 test cases with identical output.
Replaced generator-based implementation with a state machine approach:
Key Improvements:
- Eliminated generator overhead (~95ns → ~10ns per event)
- IteratorResult object reuse (zero allocations)
- Pending event queue for self-closing tag handling
- 95%+ code reuse from existing parsing logic
Benchmark Results (10MB XML file):
- Generator baseline: 115.98ms
- State machine: 92.00ms
- Improvement: +20.67%
Replaced Array-based queue with a circular buffer implementation:
Key Improvements:
- Eliminated O(n) Array.shift() operations → O(1) dequeue
- Improved memory locality with circular buffer pattern
- Queue operation cost reduced from 50ns to 10ns
- Dynamic queue growth when needed
Benchmark Results (1GB XML file):
- Array-based queue: baseline
- Circular buffer: ~15% faster
- All 796 tests passed with 100% API compatibility
- Writer performance improved by 20-60% depending on XML structure
- Sync parser performance improved by 20.67%
- Async parser performance improved by ~15% on large files
For detailed performance analysis and benchmarking methodology, see:
- Writer optimization:
packages/benchmark/writer-optimization/results/FINAL-ANALYSIS.md - Parser optimization:
packages/benchmark/PARSER_OPTIMIZATION_FINAL_REPORT.md
- Removed unused internal methods
- Enhanced benchmarking infrastructure
- Comprehensive documentation of optimization attempts and learnings
- Improved NPM publish workflow for monorepo
- Updated package README with converter features
- Configured NPM authentication for pnpm publish
- Added canvaskit-wasm dependency for astro-og-canvas
- Added frontmatter to TypeDoc generated files for Astro
- Resolved TypeScript type assertion errors in XmlArraySchema
- Added specs/ to gitignore
- Replaced eval-based ConverterDemo with iframe embed for better security
(Release notes to be added for previous versions)