stream-json

repository·master·Indexed 22 days ago

https://github.com/uhop/stream-json

A micro-library of stream components for building custom JSON and JSONC processing pipelines with a minimal memory footprint. It uses a SAX-inspired token API to parse, filter, and transform JSON documents far larger than available memory. Compatible with Node.js, Bun, Deno, and Web Streams, it provides a pipeline architecture consisting of parsers, filters (pick, replace, ignore), and streamers to assemble tokens into JavaScript objects.

Tokens
26.1K
Snippets
87
Records
114
Agent score
78%

What's inside stream-json

  1. How the Emitter works in Node.js vs Web environments

    master

    The emitter component allows you to subscribe to specific JSON tokens as events. While the conceptual model is the same (token-name as event name, token-value as event payload), the implementation differs based on the substrate:

    Node.js Emitter

    Extends Writable (an EventEmitter). Consumers subscribe using the standard .on(name, fn) pattern.

    Web Emitter

    Returns an EventTarget with a .writable WritableStream attached. Consumers subscribe using the standard .addEventListener(name, ev => ev.detail) pattern.

    This distinction allows stream-json to work seamlessly across Node.js, Bun, Deno, and modern browsers without requiring polyfills.

    /* Node.js subscription pattern */
    const emitter = new Emitter();
    emitter.on('token-name', (value) => {
      console.log(value);
    });
    
    /* Web/Browser subscription pattern */
    const emitter = new WebEmitter();
    emitter.addEventListener('token-name', (ev) => {
      console.log(ev.detail);
    });
  2. Understand the functional style migration in stream-json

    master

    The stream-json library is migrating from a class-based OOP approach (extending Node.js Transform, Writable, or Duplex classes) to a functional composition approach using stream-chain patterns.

    Why this matters:

    • Portability: Functional logic (using flushable or generator functions) is decoupled from the Node.js stream lifecycle.
    • Web Streams Readiness: Pure functions can be easily wrapped in a TransformStream for the WHATWG Streams API, whereas class-based Node streams require significant restructuring to migrate to Web Streams.
    • Usage Patterns: Most modules now export a factory function for use with chain() and a .asStream() method for use with .pipe().
  3. Understand the Token Protocol

    master

    The core of stream-json is a SAX-inspired token protocol. The parser produces a stream of {name, value} tokens that serve as the universal interchange format for all downstream components like filters, streamers, and stringers.

    Common tokens include:

    • Structural: startObject, endObject, startArray, endArray, startKey, endKey.
    • Values: keyValue (packed key), startString/endString/stringChunk/stringValue (for strings), startNumber/endNumber/numberChunk/numberValue (for numbers), nullValue, trueValue, falseValue.

    Downstream components use specific type aliases for these stages:

    • TokenSource: texttokens
    • TokenTransform: tokenstokens
    • TokenConsumer<Item>: tokensitems
    • TokenStringer: tokenstext
  4. How the Assembler reviver API works

    master

    The Assembler provides a reviver function similar to JSON.parse(), allowing you to transform or replace values as they are being assembled.

    Unlike the standard JSON.parse() spec, stream-json implements a scaled-down version that supports this binding but does not support the third context parameter.

    this Binding Behavior

    When the reviver is called, the this context is set based on the location of the value being processed:

    • Object properties: this is the partially assembled containing object.
    • Array elements: this is the array being assembled.
    • Root primitives: this is {'': value} and the key is ''.
    • Root objects/arrays: this is {'': value} and the key is ''. This allows the reviver to transform or replace the entire root value.

    Important Deviations from the JSON.parse Spec

    1. Partially Assembled this: In standard Node.js JSON.parse, this refers to a fully assembled object. In stream-json, this refers to the object containing only the properties seen so far. This is an intentional design choice to maintain streaming performance.
    2. No context parameter: The standard spec passes a context object (containing {source}) for primitives. stream-json omits this parameter.
  5. Understand the functional style migration plan

    master

    The project is migrating from a class-based OOP approach (Node Streams) to a functional composition approach (Web Streams) using stream-chain as a bridge. The migration follows a tiered complexity model:

    • Tier 1 (Trivial): Direct delegation to stream-chain primitives.
    • Tier 2 (Medium): Rewritten using gen() pipelines or specific stream-chain primitives.
    • Tier 3 (Complex): Rewritten using patterns like flushable + asStream() or complex generator compositions.

    Modules like emitter.js are exceptions because they require a stream reference to handle events, making a purely functional form difficult.

  6. Status of JsonlParser and JsonlStringer

    master

    The JsonlParser and JsonlStringer modules in stream-json have been re-implemented using stream-chain internals, but their public interfaces remain identical.

    • JsonlParser: No action is required. It maintains its existing API, including errorIndicator, checkErrors, and the checkedParse() static method.
    • JsonlStringer: No action is required. It maintains its existing API, though it is now powered by the more feature-rich stream-chain/jsonl/stringerStream.js (which supports additional options like prefix, suffix, space, and emptyValue).
  7. How JSONC support integrates with existing components

    master

    The JSONC implementation is designed for seamless integration with the rest of the stream-json ecosystem.

    • Downstream Compatibility: All existing components such as filters, streamers, and the assembler are fully compatible with the JSONC parser. Because these components are designed to ignore tokens they do not recognize, they will simply skip whitespace and comment tokens without requiring any code changes.
    • Parser/Stringer Relationship: The JSONC parser is a fork of the standard parser, and the JSONC stringer is a fork of the standard stringer. This separation ensures that the performance of the standard JSON parser is not impacted by the additional logic required for JSONC.
  8. Understand the stream-json architecture and runtime compatibility

    master

    stream-json is a micro-library designed for creating custom JSON processing pipelines with a minimal memory footprint, allowing you to parse JSON files that exceed available memory. It uses stream-chain for pipeline composition.

    The library uses a tri-tree structure to support different runtimes:

    1. core/ (Stream-agnostic): Pure factories with no Node.js-specific imports. Use these for portable logic.
    2. src/ (Node-flavored): Wrappers for Node.js and Bun. These attach both .asStream (Node Duplex) and .asWebStream (Web {readable, writable} pair) to components.
    3. src/web/ (Browser-leaning): Wrappers for browser environments. These attach only .asWebStream to ensure no Node.js-specific code is included in bundles.

    When building for a specific environment, import from the corresponding entry point to ensure optimal compatibility and bundle size.

    /* Example of choosing the right entry point based on runtime */
    
    // For Node.js or Bun (supports both Node and Web streams)
    import { parser } from 'stream-json';
    
    // For Browser environments (Web streams only)
    import { parserWebStream } from 'stream-json/web';
    
    // For pure, stream-agnostic logic
    import { parser as coreParser } from 'stream-json/core/parser';
  9. Identify unique stream-json modules

    master

    stream-json provides several core modules that are unique to its token-based architecture and do not have direct equivalents in stream-chain. These include:

    • parser.js: A SAX-like JSON tokenizer.
    • assembler.js: Reconstructs tokens into objects (EventEmitter).
    • disassembler.js: Converts objects into a token stream.
    • stringer.js: Converts tokens into complex JSON text.
    • emitter.js: Converts tokens into events (Writable stream).
    • filters/*: Token-stream editors for manipulating the token protocol.
    • streamers/*: Token-stream assemblers.
    • utils/verifier.js: Standalone JSON validation.
  10. How stream-json components work together

    master

    The library uses a pipeline architecture where each component acts as a single stage. You compose these stages using stream-chain to process massive JSON files with a minimal memory footprint.

    Typical pipeline flow:

    1. Parser: Turns raw text/bytes into a token stream.
    2. Filters: Trims or reshapes the stream (e.g., using pick to descend into specific keys or ignore to drop unwanted parts).
    3. Streamers: Assemble the surviving tokens back into usable JavaScript objects.

    This approach allows you to process documents far larger than available RAM because bytes that are skipped by filters are never assembled into memory.

  11. Use the functional API pattern for stream-json modules

    master

    Most rewritten modules in stream-json follow a consistent dual-interface pattern to support both functional chaining and standard Node.js piping:

    1. Functional Chaining: Call the factory function fn(options) to get a flushable function or a generator pipeline. This is intended for use with stream-chain's chain() method.
    2. Node.js Piping: Call fn.asStream(options) to get a standard Node.js stream (e.g., Duplex, Transform, or Writable) that can be used with .pipe().
    3. Convenience Aliases: Some modules provide named aliases for easier access.

    Note: The .make() method has been removed in favor of this pattern.

  12. Use FlexAssembler to substitute custom containers

    master

    While the standard Assembler creates plain objects {} and arrays [], FlexAssembler allows you to substitute custom containers (like Map, Set, or custom classes) at specific JSON paths. This is useful when you want to change how data is structured during the assembly process without manual post-processing.

    Rules are applied based on a filter. The first matching rule in your rules array takes precedence. If no rule matches a path, the assembler falls back to standard {} or [] behavior.

    // Example: Using a Map for all objects
    const asm = new FlexAssembler({
      objectRules: [
        {
          filter: () => true,
          create: () => new Map(),
          add: (map, key, value) => map.set(key, value)
        }
      ]
    });