seroval

repository·main·Indexed 20 days ago

https://github.com/lxsmnsyc/seroval

A JavaScript library for stringifying complex values, supporting circular references, recursive structures, and built-in types like Map, Set, RegExp, and BigInt. It provides sync, async, and streaming execution modes, as well as JSON-based serialization for sanitized client-to-server communication. The companion package seroval-plugins/web adds support for Web APIs such as Blob, File, Request, and Response.

Tokens
14.9K
Snippets
53
Records
63
Agent score
72%

What's inside seroval

  1. How reference deduping and cyclic references work

    main

    Seroval automatically handles object references. If the same object instance is encountered multiple times, it is deduped in the output. It also natively supports cyclic references (objects referencing themselves) and mutual cycles (objects referencing each other), as well as detecting potential temporal dead zones during serialization.

    import { serialize } from 'seroval';
    
    // Deduping
    const parent = {};
    const a = { parent };
    const b = { parent };
    console.log(serialize([a, b])); // (h=>([{parent:h={}},{parent:h}]) )()
    
    // Cyclic
    const cyclic = {};
    cyclic.self = cyclic;
    console.log(serialize(cyclic)); // (h=>(h={},h.self=h,h))()
  2. Use seroval-plugins/web for Web API polyfills/support

    main

    The seroval-plugins/web module provides support or polyfills for standard Web APIs. This is useful when running seroval in environments where these APIs might be missing or need specific handling. Supported APIs include:

    • AbortSignal
    • Blob
    • CustomEvent
    • DOMException
    • Event
    • File
    • FormData
    • ImageData
    • Headers
    • ReadableStream
    • Request
    • Response
    • URLSearchParams
    • URL
  3. Use isomorphic references with createReference

    main

    In isomorphic environments (where code runs on both client and server), certain values like functions cannot be serialized directly. seroval provides createReference to handle this by mapping a unique string identifier to a specific value.

    When you use createReference, seroval does not serialize the actual value (e.g., the function body); instead, it serializes the unique string identifier you provided. During deserialization, seroval uses that identifier to map back to the corresponding value present in the local environment.

    Constraints:

    • createReference only accepts objects, functions, or symbols.
    • It only serializes the identifier string, not the underlying value's content.
    import { createReference, toJSON, fromJSON } from 'seroval';
    
    // 1. Create a reference with a unique identifier
    const myIsomorphicFunction = createReference(
      'my-function', 
      () => {
        // This function must exist on both server and client
        console.log('Hello from the isomorphic function!');
      }
    );
    
    // 2. Serialize the reference
    // The output will contain the string 'my-function' rather than the function body
    const serialized = toJSON(myIsomorphicFunction);
    
    // 3. Deserialize the reference
    // This returns the actual function instance available in the current environment
    const deserialized = fromJSON(serialized);
    
    console.log(myIsomorphicFunction === deserialized); // true
  4. Disable serialization features using `disabledFeatures`

    main

    You can control how seroval processes and emits values by passing a disabledFeatures option to the serialize function. This option uses bit flags to disable specific JavaScript features or behaviors during the serialization process.

    To disable multiple features simultaneously, use the bitwise OR operator (|). By default, all feature flags are enabled.

    import { serialize, Feature } from 'seroval';
    
    // Disabling multiple features using bitwise OR
    const DISABLED_FEATURES = Feature.AggregateError | Feature.BigIntTypedArray;
    
    const result = serialize(myValue, {
      disabledFeatures: DISABLED_FEATURES,
    });
  5. Understand the Seroval Node structure

    main

    The Seroval serialization tree is composed of nodes that follow the SerovalBaseNode interface. Every node represents a piece of serialized data and contains metadata about its type, identity, and value.

    Key fields in SerovalBaseNode include:

    • t: The SerovalNodeType (e.g., Number, String, Object, Map).
    • i: A unique reference ID (number) used for identity and stateful objects.
    • s: The actual serialized value (e.g., a string, number, or unknown).
    • c: Contextual information like constructor names, RegExp sources, or Temporal types.
    • m: Messages or flags.
    • p: Properties for objects (SerovalObjectRecordNode).
    • e: Entries for Maps (SerovalMapRecordNode).
    • a: An array of child nodes.
    • f: A reference to a fulfilled or related node.
    • b: Byte offset.
    • o: SerovalObjectFlags.
    • l: Length.

    Nodes are categorized into SerovalSyncNode (synchronous data) and SerovalAsyncNode (asynchronous primitives like Promises and Streams).

    export interface SerovalBaseNode {
      t: SerovalNodeType;
      i: number | undefined;
      s: unknown;
      c: string | SerovalTemporalType | undefined;
      m: string | undefined;
      p: SerovalObjectRecordNode | undefined;
      e: SerovalMapRecordNode | undefined;
      a: (SerovalNode | 0)[] | undefined;
      f: SerovalNode | undefined;
      b: number | undefined;
      o: SerovalObjectFlags | undefined;
      l: number | undefined;
    }
  6. Identify SerovalAsyncNode types

    main

    SerovalAsyncNode represents nodes involved in asynchronous operations. These are distinct from synchronous nodes and include:

    • SerovalPromiseNode: Represents a Promise, including its state (s: 0 or 1) and its fulfilled value (f).
    • SerovalPromiseConstructorNode: Represents the Promise constructor.
    • SerovalPromiseResolveNode / SerovalPromiseRejectNode: Represent the resolution or rejection of a promise, containing the resolver/rejecter and the resolved/rejected value.
    • SerovalStreamConstructorNode: Represents a Stream constructor.
    • SerovalStreamNextNode / SerovalStreamThrowNode / SerovalStreamReturnNode: Represent the lifecycle steps of a stream (next value, error thrown, or return value).
  7. Use createStream for multi-value streaming

    main

    createStream is a specialized primitive for handling data that resolves to multiple values over time (unlike a standard Promise). It supports buffering and provides an event-based listener for next, throw, and return events. createStream instances are themselves serializable and compatible with crossSerializeStream.

    import { createStream, crossSerializeStream } from 'seroval';
    
    const stream = createStream();
    
    stream.on({
      next(data) { console.log('NEXT', data); },
      throw(data) { console.log('THROW', data); },
      return(data) { console.log('RETURN', data); },
    });
    
    stream.next('foo');
    stream.next('bar');
    stream.return('baz');
    
    // Also works with streaming serialization
    crossSerializeStream(stream, {
      onSerialize(data) { console.log(data); }
    });
  8. Basic and Async Serialization

    main

    Use serialize for synchronous data and serializeAsync when your data contains asynchronous values like Promise instances. Both methods return a serialized string representation of the input object.

    import { serialize, serializeAsync } from 'seroval';
    
    // Synchronous
    console.log(serialize({ foo: 'bar' })); // {foo:"bar"}
    
    // Asynchronous
    console.log(await serializeAsync(Promise.resolve({ foo: 'bar'}))); // Promise.resolve({foo:"bar"})
  9. Use the serialize function

    main

    Import serialize from seroval to convert JavaScript values into a string representation. Unlike standard JSON, seroval supports complex types including Map, Set, RegExp, Date, BigInt, and values with circular or mutual references.

    import { serialize } from 'seroval';
    
    const object = {
      number: [Math.random(), -0, NaN, Infinity, -Infinity],
      string: ['hello world', '<script>Hello World</script>'],
      boolean: [true, false],
      null: null,
      undefined: undefined,
      bigint: 9007199254740991n,
      array: [,,,], // holes
      regexp: /[a-z0-9]+/i,
      date: new Date(),
      map: new Map([['hello', 'world']]),
      set: new Set(['hello', 'world']),
    };
    
    // Handle circular/recursive references
    object.self = object;
    object.array.push(object.array);
    
    const result = serialize(object);
    console.log(result);
  10. Re-isolating cross-references with scopeId

    main

    When using crossSerialize or crossSerializeAsync, you can provide a scopeId string. This allows the $R global variable to be scoped (e.g., $R['A']), preventing different serialization contexts from colliding. You can initialize specific scopes using getCrossReferenceHeader(scopeId).

    import { crossSerialize, getCrossReferenceHeader } from 'seroval';
    
    const refsA = new Map();
    const refsB = new Map();
    
    // Scoped to 'A'
    console.log(crossSerialize(nodeA, { refs: refsA, scopeId: 'A' }));
    
    // Scoped to 'B'
    console.log(crossSerialize(nodeA, { refs: refsB, scopeId: 'B' }));
    
    // Initialize a specific scope
    console.log(getCrossReferenceHeader('A')); // (self.$R=self.$R||{})["A"]=[]