npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

jsyn

v1.11.22

Published

JSyn — JSON for streams, with type safety and atomic operations

Downloads

313

Readme

🌀 JSYN — a Next-Generation Streaming Data Format

JSYN is a universal streaming serialization format for JavaScript. It’s designed not just to store data, but to work with it in real time — allowing read and write operations directly from a stream, at any nesting level.

💡 Key Features

🔥 True streaming on every level — each nested object or array behaves as an independent stream. There’s no need to load the entire structure into memory or know its total size in advance.

⚡ Partial read and write support — data can be processed as it arrives, before the full document is available. This makes JSYN a perfect foundation for network protocols and streaming formats.

🧩 Structural universality — there’s no “special” document type. Every level of data behaves the same, whether it’s the root, an array, or a nested object.

♻️ Zero redundancy — JSYN doesn’t enforce its own built-in types. All values are read as plain JavaScript types, and extended types can be added flexibly through user-defined hooks.

Table of Contents

API Reference

👩🏽‍💻Installation

Installing

Install JSyn via npm:

npm i jsyn

Main Imports

Import both Writer and Reader from the main package:

import { Writer, Reader } from 'jsyn';
// or
import JSYN from 'jsyn';
const writer = new JSYN.Writer();
const reader = new JSYN.Reader();

Writer Only

If you only need writing capabilities:

import { Writer } from 'jsyn/writer';

Reader Only

If you only need reading capabilities:

import { Reader } from 'jsyn/reader';

Drivers (e.g., BSON)

For integration with BSON or other drivers:

import { create } from 'jsyn/bson';

👩🏼‍🔬Quick Start

Writing Data to Memory

import {Writer} from 'jsyn';

const writer = new Writer();

// Write a simple object
writer.push({ name: "Alice", age: 30 });

// Finalize and get the buffer
const buffer = writer.buffer();

console.log(buffer); // Buffer

Reading Data from Memory

import { Reader } from 'jsyn';

const reader = new Reader(buffer);

// Read a single value
const data = await reader.value();
console.log(data); // { name: "Alice", age: 30 }

// Read all values (if multiple)
const allData = await reader.rest();
console.log(allData);

Streaming Example

import { Writer } from 'jsyn';
import fs from 'fs';

const fileWriter = new Writer('data.jsyn');
fileWriter.push({ event: 'start' });
fileWriter.push({ event: 'progress', value: 50 });
fileWriter.push({ event: 'end' });
await fileWriter.close();
import { Reader } from 'jsyn';
import fs from 'fs';

const fileReader = new Reader('data.jsyn');
for await (const item of fileReader.values()) {
  console.log(item);
}

JSYN provides simple one-liner methods for encoding and decoding values

import JSYN from 'jsyn';

// Encode values into a single buffer
const buf = JSYN.encode(1, 2, "hello", true);

// Decode values back from the buffer
const values = await JSYN.decode(buf);

console.log(values); // [1, 2, "hello", true]

💎Supported Types

JSyn supports two main categories of types: Primitives and Containers.


Primitive Types

Primitives must be written and read atomically (completely).

| Type | Native Type | Description | |----------------|-------------|-------------| | NULL | NULL | Represents null. | | BOOLEAN | TRUE, FALSE | true or false. | | NUMBER | INT8, INT16, INT32, INT64, FLOAT32, FLOAT64 | Numeric values. JSyn automatically selects the optimal format when writing; always read as number. Supports: int16, int32, int64, float32, float64. | | BIGINT | BIGINT_POS, BIGINT_NEG | Arbitrary-size integer. Supports sizes up to 2^28-1 bytes. | | STRING | STRING | UTF-8 encoded string. Maximum length: 0 to 2^28-1 bytes. | | DATE | DATE | JavaScript Date object. | | REGEXP | REGEXP | JavaScript RegExp object. | | BINARY | BINARY | Binary data. Supports Buffer, ArrayBuffer, SharedArrayBuffer, and all BufferViews (TypedArray, DataView). When read in Node.js, always returns Buffer. Maximum size: 0 to 2^28-1 bytes. | | EXTENDED | EXTENDED | Custom user-defined type. Written as JSYN.Value(typeid, payload); when read, handled by reader.exts. By default, returns JSYN.Value(). |


Container Types

Containers can be partially or fully read/written. They may represent potentially infinite sequences.

| Type | Description | |----------------|---------------| | OBJECT | JavaScript object ({}). Keys are UTF-8 strings with maximum size 0 to 2^28-1 bytes. | | ARRAY | JavaScript array ([]). | | MAP | JavaScript Map. | | SET | JavaScript Set. | | STREAM | No direct JS equivalent. Container for binary data. Fully read → BINARY. Partially read → sequence of Buffer chunks. | | TEXT | No direct JS equivalent. Container for textual data. Fully read → STRING. Partially read → sequence of STRING chunks. |


Unsupported Types

Some JavaScript values cannot be serialized in JSYN. Attempting to write them will either throw a non-fatal error or result in a fallback (e.g. ignored value).

| Type | Behavior | |-----------------|-------------------------------------------------------| | undefined | Ignored or replaced with null depending on context. | | symbol | Ignored or replaced with null depending on context. | | function | Ignored or replaced with null depending on context. |


JSYN Native Types

These are the low-level identifiers used internally by the JSYN protocol. They represent the exact encoding format and are used by readers and writers.

| Type | Code (Hex) | Description / Value Range | |------------------|------------|---------------------------| | END | 0x00 | End of container marker. | | NULL | 0x01 | Represents null. | | FALSE | 0x02 | Boolean false. | | TRUE | 0x03 | Boolean true. | | INT8 | 0x04 | Signed 8-bit integer. Range: -128 to 127. | | INT16 | 0x05 | Signed 16-bit integer. Range: -32768 to 32767. | | INT32 | 0x06 | Signed 32-bit integer. Range: -2^31 to 2^31-1. | | INT64 | 0x07 | Signed 64-bit integer. Range: -2^63 to 2^63-1. | | FLOAT32 | 0x08 | 32-bit floating point number. | | FLOAT64 | 0x09 | 64-bit floating point number. | | BIGINT_POS | 0x0A | Positive arbitrary-size integer. Maximum size: 2^28-1 bytes. | | BIGINT_NEG | 0x0B | Negative arbitrary-size integer. Maximum size: 2^28-1 bytes. | | STRING | 0x0C | UTF-8 encoded string. Maximum length: 0 to 2^28-1 bytes. | | DATE | 0x0D | JavaScript Date object. Signed 64-bit integer. | | REGEXP | 0x0E | JavaScript RegExp object. | | BINARY | 0x0F | Binary data (Buffer, ArrayBuffer, TypedArray, DataView). Maximum size: 0 to 2^28-1 bytes. | | EXTENDED | 0x10 | Custom user-defined type. Written as JSYN.Value(typeid, payload). | | OBJECT | 0x11 | JavaScript object ({}). | | MAP | 0x12 | JavaScript Map. | | ARRAY | 0x13 | JavaScript array ([]). | | SET | 0x14 | JavaScript Set. | | STREAM | 0x15 | Sequence of binary chunks. Fully read → BINARY. | | TEXT | 0x16 | Sequence of string chunks. Fully read → STRING. |


Surrogate Type Categories

These are higher-level groupings of JSYN native types, used for classification and conditional handling in readers and writers. Each category has a corresponding IS_* method that returns true if the type belongs to the group.

| Category / Method | Includes Native Types | Description | |------------------------|-----------------------------------------------------------------|-------------| | IS_END(type) | END | Marks the end of a container. | | IS_TYPE(type) | All JSYN types | Any valid JSYN type. | | IS_PRIMITIVE(type) | NULL, FALSE, TRUE, INT8, INT16, INT32, INT64, FLOAT32, FLOAT64, BIGINT_POS, BIGINT_NEG, STRING, DATE, REGEXP, BINARY, EXTENDED | Atomic values that are always read/written fully. | | IS_PRIMITIVE_OBJECT(type) | DATE, REGEXP, BINARY, EXTENDED | Special primitive-like objects with internal structure. | | IS_NUMBER(type) | INT8, INT16, INT32, INT64, FLOAT32, FLOAT64 | Numeric types. | | IS_INT(type) | INT8, INT16, INT32, INT64 | Signed integer types. | | IS_FLOAT(type) | FLOAT32, FLOAT64 | Floating point numbers. | | IS_BIGINT(type) | BIGINT_POS, BIGINT_NEG | Arbitrary-size integers. | | IS_EXT(type) | EXTENDED | User-defined extension types. | | IS_CONTAINER(type) | OBJECT, MAP, ARRAY, SET, STREAM, TEXT | All container types. | | IS_COLLECTION(type) | ARRAY, SET | Group of value-sequence containers. | | IS_DICTIONARY(type) | OBJECT, MAP | Key-value mapping containers. | | IS_FLOW(type) | STREAM, TEXT | Sequential stream/text containers. | | IS_SEQUENCE(type) | ARRAY, SET, STREAM, TEXT | Any sequential container (values or chunks). | | IS_OBJECT(type) | OBJECT | Plain JavaScript object. | | IS_MAP(type) | MAP | Key-value map. | | IS_ARRAY(type) | ARRAY | Array of values. | | IS_SET(type) | SET | Set of values. | | IS_STREAM(type) | STREAM | Stream of binary chunks. | | IS_TEXT(type) | TEXT | Stream of text chunks. |


Type Utility Methods

Value-based methods

| Method | Description | |--------|-------------| | JSYN.typeOf(value) | Returns the internal numeric type code of any value. See Supported Types. | | JSYN.isSupported(value) | Returns true if the value can be serialized by JSYN. | | JSYN.isExt(value) | Returns true if the value is a user-defined JSYN.Value (extended type). |

Type-based methods

| Method | Description | |--------|-------------| | types.RAW_TYPE(typeId) | Returns the internal protocol name of the given type identifier (string). | | types.TYPE(typeId) | Returns a human-readable extended type name for the given type identifier. | | types.KIND(typeId) | Returns a generic type name compatible with JS or other languages. | | types.TYPE_ID(value) | Returns a internal protocol type id for the given type name | | types.IS_*(typeId) | Boolean classifiers for type groups or individual types. |

Reader methods

| Method | Description | |--------|-------------| | reader.rawId() | Returns the numeric type ID of the next value in the current scope (or 0 if none). | | reader.raw() | Returns the internal protocol name of the next value (or 'end' if none). | | reader.type() | Returns the human-readable extended type name of the next value (or undefined if none). | | reader.kind() | Returns the JS/compatible type name of the next value (or undefined if none). |

JSYN Names Type Reference

| typeId / rawId() | RAW_TYPE(t) / reader.raw() | TYPE(t) / reader.type() | KIND(t) / reader.kind() | |-----------------|----------------------------|-------------------------|------------------------| | 0x00 | 'end' | undefined | undefined | | 0x01 | 'null' | 'null' | 'null' | | 0x02 | 'false' | 'boolean' | 'boolean' | | 0x03 | 'true' | 'boolean' | 'boolean' | | 0x04 | 'int8' | 'number' | 'number' | | 0x05 | 'int16' | 'number' | 'number' | | 0x06 | 'int32' | 'number' | 'number' | | 0x07 | 'int64' | 'number' | 'number' | | 0x08 | 'float32' | 'number' | 'number' | | 0x09 | 'float64' | 'number' | 'number' | | 0x0A | 'bigint_pos' | 'bigint' | 'bigint' | | 0x0B | 'bigint_neg' | 'bigint' | 'bigint' | | 0x0C | 'string' | 'string' | 'string' | | 0x0D | 'date' | 'date' | 'object' | | 0x0E | 'regexp' | 'regexp' | 'object' | | 0x0F | 'binary' | 'binary' | 'object' | | 0x10 | 'extended' | 'extended' | 'object' | | 0x11 | 'object' | 'object' | 'object' | | 0x12 | 'map' | 'map' | 'object' | | 0x13 | 'array' | 'array' | 'object' | | 0x14 | 'set' | 'set' | 'object' | | 0x15 | 'stream' | 'stream' | 'object' | | 0x16 | 'text' | 'text' | 'string' | | stream chunk | 'binary' | 'binary' | 'object' | | text chunk | 'string' | 'string' | 'string' |

🎁Extended Types

JSYN allows using custom (extended) types in addition to the built-in types. Extended types let you assign a separate class or type for writing and reading atomic data.

User-defined data consists of two fields: typeId and payload.

1. typeId

  • Integer identifier of the extended type.
  • Values range from 1 to JSYN.MAX_EXTENDED_TYPE_ID = 268435455 (2^28 - 1).
  • Value 0 is reserved and cannot be used.
  • All other values are available, but to avoid collisions it is recommended to follow the table:

| Range | Usage | |-----------------|-----------------------------------------------| | 1–63 | Reserved for user space | | 64–79 | Reserved for BSON | | 80–127 | Reserved for other standard extended types | | 128–268435455 | Grey zone, currently undefined |

Note: The typeId space does not conflict with system types, but following the table is recommended to avoid collisions.

2. payload

  • Any JSYN-supported value (<VALUE> ::= <TYPE> <DATA>), e.g. null, binary, string, array, object, etc.
  • Stored as a normal value, but with two differences:
    1. Hooks applied to system values are not applied to payload or its nested data.
    2. Cannot be written or read partially like containers — only as a whole:
writer.ext(1, { key: 1 });

Writing Extended Data

There are two ways to write extended data:

Explicit

writer.ext(typeId, payload);
  • Can be called wherever a value is expected (e.g., where writer.value() is allowed).

Implicit

  • Any data wrapped with the JSYN.Value class is saved as an extended type.
  • You can create a value using:
import {ext, Value} from 'jsyn'

ext(typeId, payload);           // simple method
ext(typeId);                    // payload optional
new Value(typeId, payload);      // using class

Implicit Interpretation of Data as Extended Type

To interpret a custom class (or other type) as an extended type during writing:

  1. Implement toJSYN() for objects:
class MyData {
  toJSYN() {
    return JSYN.ext(1234, "payload");
  }
}
writer.value(new MyData());
  1. Use hooks:
function MyHook(key, value) {
  if (value instanceof MyData)
    return JSYN.ext(1234, "payload");
  return value;
}
writer.hook(MyHook);
writer.value(new MyData());

Reading Extended Types

  • When an extended type is found, the parser uses the type registry reader.exts.
  • If a handler is registered for the type, it is applied.
  • If no handler is registered or an error occurs, the value is wrapped in the JSYN.Value class.

Assigning a Handler for Extended Types

function MyHandler(value) {
  return new MyData(value);
}

// Register handler
reader.exts.set(1234, MyHandler);

// Or using shorthand
reader.register(1234, MyHandler);
reader.register(1234, value => new MyData(value));

📚BSON Integration

JSYN provides optional integration with BSON (e.g., from the mongodb package) via a helper module. This allows BSON types to be serialized as extended JSYN values and deserialized back to their original BSON representations.

Usage Example

// Import BSON from MongoDB driver
import { BSON } from 'mongodb';

// Import JSYN BSON helper
import { create } from 'jsyn/bson';

// Create BSON hooks and type registry
const bson = create(BSON);

// Access the hook for writer and type map for reader
bson.Hook  // Hook for Writer to wrap BSON types into JSYN.Value
bson.Types // TypeMap for Reader to convert JSYN.Value back to BSON

// Create Writer and Reader instances
import { Writer, Reader } from 'jsyn';
const writer = new Writer();
const reader = new Reader();

// Attach BSON hook to Writer
writer.hook(bson.Hook);

// Attach BSON type handlers to Reader
reader.registerAll(bson.Types);

// Write BSON value
writer.push(new BSON.ObjectId());

// Finish writer buffer and send to reader
reader.finish(writer.buffer());

// Read back the BSON value
const oid = await reader.value();
console.log(oid); // ObjectId

Notes

  • writer.hook(bson.Hook) automatically wraps supported BSON types in JSYN.Value.
  • reader.registerAll(bson.Types) allows the reader to convert JSYN extended types back to BSON.
  • Works seamlessly with all JSYN Writer/Reader methods.

🔬Binary Format

JSyn uses a streamable tagged binary format. Each value is represented as <VALUE> ::= <TYPE> <DATA>. The <DATA> can be either a primitive or a container.

Root of the JSyn binary format

<JSYN_ROOT> ::= (<VALUE>)* END
<JSYN_ROOT> ::= (<VALUE>)* EOF

VALUE is a type byte followed by type-specific data

<VALUE> ::= <TYPE> <DATA>

EOF marker

EOF ::= <end of data>   # represents the natural end of the data stream (no more bytes to read)

End marker

END ::= 0x00 # indicates end of any container/scope

Type identifiers (1 byte)

<TYPE> ::= 0x01..0xFF
| TYPE_NULL = 0x01
| TYPE_FALSE = 0x02
| TYPE_TRUE = 0x03
| TYPE_INT8 = 0x04
| TYPE_INT16 = 0x05
| TYPE_INT32 = 0x06
| TYPE_INT64 = 0x07
| TYPE_FLOAT32 = 0x08
| TYPE_FLOAT64 = 0x09
| TYPE_BIGINT_POS = 0x0A
| TYPE_BIGINT_NEG = 0x0B
| TYPE_STRING = 0x0C
| TYPE_DATE = 0x0D
| TYPE_REGEXP = 0x0E
| TYPE_BINARY = 0x0F
| TYPE_EXTENDED = 0x10
| TYPE_OBJECT = 0x11
| TYPE_MAP = 0x12
| TYPE_ARRAY = 0x13
| TYPE_SET = 0x14
| TYPE_STREAM = 0x15
| TYPE_TEXT = 0x16

Variable-length integers (VARINT) for lengths and type IDs

<VARINT> ::= 1-4 bytes
  • 1 byte: 0xxxxxxx (0–127)
  • 2 bytes: 1xxxxxxx 0xxxxxxx (0–16_383)
  • 3 bytes: 1xxxxxxx 1xxxxxxx 0xxxxxxx (0–2_097_151)
  • 4 bytes: 1xxxxxxx 1xxxxxxx 1xxxxxxx 0xxxxxxx (0–268_435_455)
<VARSIZE> ::= <VARINT> # length of variable-size payload
<VARTYPE> ::= <VARINT> # user-defined type identifier

DATA can be a primitive or a container

<DATA> ::= <PRIMITIVE>
| <OBJECT>
| <ARRAY>
| <SET>
| <MAP>
| <STREAM>
| <TEXT>

Primitive DATA formats

<PRIMITIVE> ::=
INT8 ::= 1 byte signed
INT16 ::= 2 bytes BE signed
INT32 ::= 4 bytes BE signed
INT64 ::= 8 bytes BE signed
FLOAT32 ::= 4 bytes BE float
FLOAT64 ::= 8 bytes BE float
BIGINT_POS ::= <VARSIZE> + bytes BE unsigned
BIGINT_NEG ::= <VARSIZE> + bytes BE unsigned
STRING ::= <VARSIZE> + UTF-8 bytes
DATE ::= 8 bytes BE (INT64)
REGEXP ::= <STRING> pattern + <STRING> flags
BINARY ::= <VARSIZE> + bytes
EXTENDED ::= <VARTYPE> + <PAYLOAD>

PAYLOAD is a complete value (can be primitive or container)

<PAYLOAD> ::= <VALUE>

Containers

<OBJECT_KEY> ::= <STRING>
<OBJECT_ENTRY> ::= <TYPE> <OBJECT_KEY> <DATA>
<OBJECT> ::= (<OBJECT_ENTRY>)* END # 0x00 marks end

<MAP_KEY> ::= <VALUE>
<MAP_VALUE> ::= <VALUE>
<MAP_ENTRY> ::= <MAP_KEY> <MAP_VALUE>
<MAP> ::= (<MAP_ENTRY>)* END # 0x00 marks end

<ARRAY> ::= (<VALUE>)* END
<SET> ::= (<VALUE>)* END

<STREAM_CHUNK> ::= <BINARY> where <VARSIZE> > 0
<STREAM> ::= (<STREAM_CHUNK>)* END # 0x00 marks end

<TEXT_CHUNK> ::= <STRING> where <VARSIZE> > 0
<TEXT> ::= (<TEXT_CHUNK>)* END # 0x00 marks end

Notes / Key Points:

  • <VALUE> = <TYPE> + <DATA>. <DATA> can be either a primitive or a container.
  • Containers (OBJECT, ARRAY, SET, MAP, STREAM, TEXT) are terminated by the END marker (0x00).
  • Partial reading/writing is supported for streams and containers — enabling potentially infinite sequences.
  • VARINT encodes lengths and type IDs efficiently (1–4 bytes).
  • Every <VALUE> is self-contained, making JSyn suitable as a streaming protocol.

🔒Root Scope END Byte

Every scope in JSYN is terminated with an END byte (0x00). In the root scope, this byte signals the end of all data, after which reading further values is meaningless.

  • In the root scope, the END byte is optional. If it is absent, the end of data is considered to be EOF (end of the provided data).

When to use END-ROOT

  • When you want to seal the root container and guarantee that no further data will follow.
  • Useful if you want to write a complete, self-contained sequence and ensure that the reader can detect its completion.
  • Ideal for network transmission, where you do not want to rely on a connection close as the end-of-data signal.

When NOT to use END-ROOT

  • If you cannot know whether more data will follow, for example:
    • Logging to a file where additional entries will be appended later. Using END-ROOT would prevent further writes.
    • Writing an infinite or continuous data stream, such as ongoing message delivery or real-time telemetry, where a formal end is not expected.

Default behavior

  • By default, END-ROOT is not emitted.

How to emit END-ROOT when writing

writer.push(v1, v2, v3);
await writer.close(true); // emits END-ROOT

Reader behavior

  • When reading, END-ROOT is treated as the end of data, equivalent to EOF.
  • After encountering END-ROOT, the reader stops processing new data and waits for reader.end() or reader.close() to formally finish and transition to the closed state.

📝Writing Data

Writer Lifecycle

The JSyn Writer has three main states and a writable flag. These define what operations are allowed at any given moment.

States Overview

  1. Opened – default state after creation.

    • Writable = true – actively writing to a sink; all write methods available.
    • Writable = falseclose() or end() (from root scope) has been called; all write methods throw errors except writer.end().
  2. Closed – writing has successfully finished.

    • All write methods throw errors except writer.end().
    • Can be reset to opened via writer.reset() or writing methods that reopen the writer.
  3. Failed – fatal error occurred; writing cannot continue.

    • Error details available in writer.error.
    • Can be reset to opened via writer.reset() or reopening methods.
    • Most operations throw errors except writer.end().

Checking Writer State

The current state can be accessed via:

  • writer.state → returns 'opened', 'closed', or 'failed'.
  • Convenience flags: writer.opened, writer.closed, writer.failed. These indicate the same information and can be used to guard write operations or handle errors.

Opened State Operations

From opened, the following actions are possible:

Not allowed / will throw non-fatal error:

Closed State Operations

From closed, the following actions are possible:

Failed State Operations

From failed, the following actions are possible:

Notes

  • The writable flag is automatically managed:
    • true when actively writing,
    • false after close() or end() in root scope.
  • Proper handling of states ensures stream safety and allows incremental or storable writing.
  • Use writer.reset() or compatible methods to reuse a writer after closed or failed states.

Writer Scopes

Each scope represents a nested writing context — a container level on the internal stack. Scopes are hierarchical; each new container (object, array, map, etc.) opens a new scope inside the current one. The current scope can be accessed via writer.scope, and the nesting level via writer.depth.

Each scope defines its own writing rules and available methods.

Scopes are opened by one of: writer.object(), writer.map(), writer.array(), writer.set(), writer.stream(), writer.text() and are closed by writer.end().

All nested scopes can be closed with writer.close().


1. Root Scope

  • Always the top-level scope (depth = 1).
  • Represents a sequence of values.
  • Unsupported values are ignored.
  • Cannot open another root scope.

Allowed methods:


2. Array Scope

  • Writes a sequence of values (array elements).
  • Unsupported values are replaced with null.
  • Opened via writer.array().

Allowed methods:


3. Set Scope

  • Writes a sequence of unique values.
  • Unsupported values are ignored.
  • Opened via writer.set().

Allowed methods:


4. Object Scope

  • Writes key–value pairs (similar to a JavaScript object).
  • Keys must be strings.
  • Invalid keys cause a non-fatal error.
  • Unsupported values are ignored.
  • Opened via writer.object().

Has two sub-states: expecting key / expecting value.

When expecting key:

When expecting value:

writer.end(...values) — if values are passed → non-fatal error.


5. Map Scope

  • Writes key–value pairs with arbitrary key types.
  • Invalid keys cause a non-fatal error.
  • Unsupported values are ignored.
  • Opened via writer.map().

Has two sub-states: expecting key / expecting value.

When expecting key:

When expecting value:

writer.end(...values) — if values are passed → non-fatal error.


6. Stream Scope

  • Writes a sequence of binary chunks.
  • Non-binary values cause a non-fatal error.
  • Opened via writer.stream().

Allowed methods:


7. Text Scope

  • Writes a sequence of string chunks.
  • Non-string values cause a non-fatal error.
  • Opened via writer.text().

Allowed methods:

Scope Method Compatibility Table

| Method / Scope | Root | Array | Set | Stream/Text | Object/Map (key) | Object/Map (value) | |--------------------|:----:|:-----:|:---:|:-----------:|:----------------:|:------------------:| | writer.value(v) |✅|✅|✅|✅|❌|✅| | writer.values(vs) |✅|✅|✅|✅|❌|❌| | writer.push(...v) |✅|✅|✅|✅|❌|❌| | writer.add(v) |❌|❌|✅|❌|❌|❌| | writer.key(k) |❌|❌|❌|❌|✅|✅| | writer.entry(k,v) |❌|❌|❌|❌|✅|✅| | writer.entries(es) |❌|❌|❌|❌|✅|✅| | writer.assign(s) |❌|❌|❌|❌|✅|✅| | writer.ext(id,p) |✅|✅|✅|❌|❌|✅| | writer.load(s) |❌|❌|❌|✅|❌|❌| | writer.file(p) |❌|❌|❌|✅|❌|❌| | writer.object() |✅|✅|✅|❌|❌|✅| | writer.array() |✅|✅|✅|❌|❌|✅| | writer.set() |✅|✅|✅|❌|❌|✅| | writer.map() |✅|✅|✅|❌|❌|✅| | writer.stream() |✅|✅|✅|❌|❌|✅| | writer.text() |✅|✅|✅|❌|❌|✅| | writer.end() |✅|✅|✅|✅|✅|✅| | writer.end(...v) |✅|✅|✅|✅|❌|❌|

Choosing a Sink

JSYN supports three types of sinks (receivers) for writing data.

Sink States Overview

| Sink Type | Stored Data | Notes | |-----------|-------------|-----------------| | Store (default) | ✅ Internal buffer | Data can be extracted via release(), buffer(), blob(). Can redirect to another sink with to(). | | Wipe | ❌ Not stored | Silent mode, useful for testing. Data is discarded. | | Pipe | ❌ Not stored internally | Data is streamed to an external sink. Switching sinks requires write completion, except with to(). |

1. Store (default)

Data extraction methods:

2. Wipe

  • Description: Data is not stored.
  • Use case: Silent mode for testing or temporary operations.
  • Activation: writer.wipe()

3. Pipe

  • Description: Data is written to an external sink.
  • Activation: writer.pipe(sink, options)
  • Supported external sinks:
    • Function (callback)
    • Object (wrapped into a web stream)
    • Node.js stream
    • Web stream
    • File

Notes:

  • Switching to a different sink is allowed only after the current write is complete.
  • Exception: writer.to(sink, options) — redirects accumulated data to another sink and continues writing if the write is not finished.

Hooks

JSYN supports hooks to customize how values are processed during writing. Hooks are functions that can transform, filter, or wrap data before it is serialized.

Adding Hooks

You can add a hook to a Writer instance using the writer.hook() method:

function myHook(key, value) {
  // Transform a specific class into an extended type
  if (value instanceof MyData) {
    return JSYN.ext(1234, value.toPayload());
  }
  // Transform a bigint values
  if(typeof value === 'bigint')
  	return value.toString();
  // Return value as-is for normal handling
  return value;
}

writer.hook(myHook);
  • key – the current key in an object/map or index in an array/set/root scope
  • value – the value to be written. Hooks are applied before the value is serialized, allowing modification or replacement.

Hooks Using

Hooks allow preprocessing of values before they are serialized. They are applied selectively depending on the scope:

  • Scopes where hooks are applied:
    • object / map — applied to values only, not keys
    • root, array, set — applied to values
  • Scopes where hooks are NOT applied:
    • stream, text — hooks do not apply
  • Hooks are not applied to payloads of extended types
  • Hooks are not applied to containers created manually via writer.object(), writer.array(), etc., but they do apply to nested values written using writer.value(), writer.push(), writer.add(), writer.entry()

Hook Execution Algorithm

For each value being written:

  1. Check for toJSYN: If the value is an object with a toJSYN property that is a function, it is called as:

    value.toJSYN(key)

    where key is the current key in scope.

  2. Apply registered hooks: If multiple hooks are registered, the output of the first hook is passed as input to the next, in sequence.

  3. Filter unsupported values: If after hooks the value is one of undefined, symbol, or function (unsupported types), it is handled as follows:

  • object/map — the key-value pair is ignored
  • root/set — the value is ignored
  • array — replaced with null

This mechanism allows hooks to filter or transform values before serialization. For example, returning undefined from a hook will remove the value from the output.

// Custom hook to filter out negative numbers
function filterNegative(key, value) {
  if (typeof value === 'number' && value < 0) return undefined;
  return value;
}

writer.hook(filterNegative);

writer.array();
writer.value(1);
writer.value(-5);  // filtered out, replaced with null in array
writer.value(3);
writer.end();  // result: [1, null, 3]

Writing Examples

import { Writer } from 'jsyn';

const writer = new Writer();

// -----------------------------
// 1. Atomic Writing (whole value)
// -----------------------------

writer.value("Simple string");        // atomic string
writer.value(123);                    // atomic number
writer.value(true);                   // atomic boolean
writer.value([1, 2, 3]);              // atomic array
writer.value({ name: "Alice" });      // atomic object
writer.value(new Set(["a","b"]));     // atomic set
writer.value(new Map([["k", 42]]));   // atomic map
writer.end();                         // closes root

// -----------------------------
// 2. Writing Containers Piece by Piece
// -----------------------------

// Array
writer.array();
writer.value(1);
writer.value(2);
writer.value(3);
writer.push(4,5,6);
writer.end();

// Object
writer.object();
writer.entry("name", "Bob");
writer.entry("age", 25);
writer.end();

// Set
writer.set();
writer.add("apple");
writer.add("banana");
writer.end();

// Map
writer.map();
writer.entry("key1", "value1");
writer.entry("key2", 42);
writer.end();

// -----------------------------
// 3. Nested Containers
// -----------------------------

writer.object();
writer.entry("user", { name: "Charlie", roles: ["admin", "editor"] }); // atomic object inside object

writer.entry("data", null); // atomic null
writer.entry("scores", [10, 20, 30]); // atomic array

writer.key("settings");     // now open nested map
writer.map();
writer.entry("theme", "dark");
writer.entry("notifications", true);
writer.end();               // closes nested map

writer.entry("tags", ["jsyn", "binary", "protocol"]); // atomic array
writer.end();               // closes root object

const buf = writer.buffer(); // release jsyn data as Buffer

// -----------------------------
// 4. Stream and Text Containers
// -----------------------------

// Stream (binary)
writer.stream();
writer.value(Buffer.from("hello "));
writer.value(Buffer.from([119,111,114,108,100]));
writer.end();

// Text (UTF-8 strings)
writer.text();
writer.value("line 1\n");
writer.value("line 2\n");
writer.end();

// -----------------------------
// 5. Loading External Sources
// -----------------------------

// Open Stream Scope
writer.stream();

// Load raw buffers / strings
await writer.load(Buffer.from("ABC"));
await writer.load("DEF");

// Load nested iterators
await writer.load([
  "chunk1",
  ["chunk2a", "chunk2b"],
  Buffer.from("chunk3")
]);

// Load readable streams
import fs from "node:fs";
await writer.load(fs.createReadStream("input.bin"));

// Load web streams
const webStream = new Response("xyz").body;
await writer.load(webStream);

// Load file directly from filesystem
await writer.file("data.bin");

// Close Stream
writer.end();

// -----------------------------
// 6. Bulk Values for Array / Root / Set
// -----------------------------

writer.array();
await writer.values([1, 2, 3, 4]);
await writer.values((async function* () {
  yield 5;
  yield 6;
})();
writer.end();

// -----------------------------
// 7. Entries and Assign for Object / Map
// -----------------------------

// object.entries(...)
writer.object();
await writer.entries([
  ["name", "Delta"],
  ["age", 30],
]);
writer.end();

// map.entries(...)
writer.map();
await writer.entries(new Map([
  [1, "one"],
  [2, "two"]
]));
writer.end();

// assign(object)
writer.object();
writer.assign({ a: 10, b: 20, c: 30 });
writer.end();

📚Reading Data

Reader Lifecycle

The JSyn Reader has three main states (opened, closed, failed) and corresponding flags (reader.opened, reader.closed, reader.failed, reader.state, reader.error).

States Overview

  1. Opened – default state; reading is active.

    • All read methods are available.
    • reader.close() → transitions to closed.
    • reader.end() or reader.rest() (in root-scope) → transitions to closed.
    • reader.abort() → transitions to failed.
    • Incorrect data termination or fatal error → transitions to failed.
    • reader.reset() → resets reading and terminates the source if possible, but does not change the state.
    • Proper data end (END marker or EOF) does not change the state; read methods simply return undefined or false.
  2. Closed – client-initiated completion of reading.

  3. Failed – fatal error occurred; further reading is impossible.

Checking Reader State

  • reader.state → returns 'opened', 'closed', or 'failed'.
  • Flags: reader.opened, reader.closed, reader.failed.
  • These reflect the same state and can be used to control flow or handle errors.

Read Methods


Type & Raw Access

  • reader.rawId()
    Returns the numeric native ID of the current value’s type (or chunk type for streams/text).

  • reader.raw()
    Returns the native type name of the current value as a string (e.g., 'int16', 'date', 'string').

  • reader.type()
    Returns the high-level type of the current value as a readable string (e.g., 'number', 'array', 'date').

  • reader.kind()
    Returns the most general, JavaScript-compatible type of the current value (e.g., 'number', 'string', 'object').


Scope Navigation

  • reader.open()
    Opens a new nested scope if the current value is a container. Returns number if a new scope was opened, otherwise 0.

  • reader.end()
    Closes the current container and skips any unread values. In the root scope, ends all reading.

  • reader.rest()
    Closes the current container and returns any remaining unread values. In the root scope, ends all reading.


Scope Shape Helpers


Bulk Reading

  • reader.save(sink, options)
    Streams the current container’s remaining data to a sink, closing the scope when done.

  • reader.each(fn)
    Applies fn to each value and closes the container when done.

  • reader.walk(fn, depth)
    Applies fn to each value recursively up to depth and closes all traversed scopes.

  • reader.keys(end)
    Returns an async iterator over keys in the current container.
    If end is true (default), closes the scope after iteration.

  • reader.values(end)
    Returns an async iterator over values in the current container.
    If end is true (default), closes the scope after iteration.

  • reader.entries(end)
    Returns an async iterator over key–value entries in the current container.
    If end is true (default), closes the scope after iteration.


Flattening Utilities

  • reader.flat(depth)
    Returns an async iterator over all values recursively.
    depth limits the nested depth to traverse.

  • reader.flatKeys(depth)
    Returns an async iterator over keys recursively.
    depth limits the nested depth to traverse.

  • reader.flatValues(depth)
    Returns an async iterator over values recursively.
    depth limits the nested depth to traverse.

  • reader.flatEntries(depth)
    Returns an async iterator over key–value entries recursively.
    depth limits the nested depth to traverse.


Return Behavior

If there are no more values in the current scope:


Reader Scopes

Reading operates through hierarchical scopes — containers that define the current reading context. The current scope is accessible via reader.scope, and the current nesting level (depth) via reader.depth. Reader behavior depends on the active scope type: root, array, set, map, stream, or text.

1. Root

  • Created by default at the top level.
  • Keys: numeric, sequential starting from 0.
  • Values: any supported values.
  • rest() / end() — terminates all reading.

2. Array

  • Keys: numeric, sequential starting from 0.
  • Values: any supported values.
  • rest() — returns an array.

3. Set

  • Keys: numeric, sequential starting from 0 (unlike JavaScript Set, which uses values as keys).
  • Values: any supported values.
  • rest() — returns a Set.

4. Object

  • Keys: strings.
  • Values: any supported values.
  • rest() — returns an object.

5. Map

  • Keys: any supported values.
  • Values: any supported values.
  • rest() — returns a Map.

6. Stream

  • Keys: numeric, sequential starting from 0.
  • Values: binary chunks (Buffer).
  • rest() — returns a concatenated Buffer.

7. Text

  • Keys: numeric, sequential starting from 0.
  • Values: string chunks.
  • rest() — returns a concatenated string.

Choosing a Source

By default, a Reader is in the reading state. If no data source is provided in the constructor, the reader waits for incoming data.

Providing Data

Data can be supplied using two methods:

  • reader.push(...chunks) — sends one or more data sources to the reader. Can be called multiple times to provide additional data.

  • reader.finish(...chunks) — sends one or more data sources and notifies the reader that no more data will be provided. After calling finish(), further calls to push() or finish() have no effect until the reader closes the current session and starts a new one.

Supported Chunk Types

Each chunk can be:

  • Binary data — Buffer, ArrayBuffer, BufferView, or anything convertible to a buffer.
  • Blob
  • Node.js stream
  • Web stream
  • String — interpreted as a file path to read from.
  • Synchronous or asynchronous iterator — each item can be any of the above, including nested iterators.

Data Processing

  • Chunks are processed lazily, only when reading methods are called.
  • They are queued internally if needed.
  • Calling reader.finish() does not immediately finish reading, as unread data may remain in the queue or internal cache.
  • Once the current reading session ends, all unprocessed queued data is discarded.

Conditions for Completing Data Processing

Processing stops when one of the following occurs:

  • reader.close() is called.
  • reader.end() or reader.rest() is called in the root scope.
  • reader.abort() is called.
  • A fatal error occurs in one of the sources or due to invalid data.
  • The end of all supplied data is reached (including all arguments passed to finish()).
  • The parser encounters the root-scope end marker END (0x00).

Reading Examples

import { Reader } from 'jsyn';

// Suppose we have a buffer with data produced by the Writer
const reader = new Reader(buffer);

// -----------------------------
// 1. Atomic Reading
// -----------------------------
console.log(await reader.value()); // "Simple string"
console.log(await reader.value()); // 123
console.log(await reader.value()); // true

// -----------------------------
// 2. Piecewise Reading with Async Iterators
// -----------------------------

// Array
if (await reader.open()) {
  for await (const value of reader.values()) {
    console.log(value); // 1, 2, 3
  }
}

// Object
if (await reader.open()) {
  for await (const [key, value] of reader.entries()) {
    console.log(key, value); // name: "Bob", age: 25
  }
}

// Set
if (await reader.open()) {
  for await (const value of reader.values()) {
    console.log(value); // "apple", "banana"
  }
}

// Map
if (await reader.open()) {
  for await (const key of reader.keys()) {
    console.log(key); // key1, key2
    console.log(await reader.value()); // "value1", 42
  }
}

// -----------------------------
// 3. Nested Containers
// -----------------------------

if (await reader.open()) {  // root object
  for await (const [key, value] of reader.entries()) {
    if (key === "user") {
      console.log(value); // atomic object
    } else if (key === "settings") {
      if (await reader.open()) {  // nested map
        for await (const [k, v] of reader.entries()) {
          console.log(k, v); // theme: "dark", notifications: true
        }
      }
    } else {
      console.log(key, value); // other atomic values
    }
  }
}

// -----------------------------
// 4. Saving Stream / Text to File or Stream
// -----------------------------

// Save to filesystem
await reader.save("output.txt");            // saves all remaining chunks to file

// Save to Node.js writable stream
import fs from 'node:fs';
const writable = fs.createWriteStream("output2.bin");
await reader.save(writable);                // writes remaining chunks to stream

// Save using a custom callback function
await reader.save(chunk => console.log("Chunk:", ch