jsyn
v1.11.22
Published
JSyn — JSON for streams, with type safety and atomic operations
Downloads
313
Maintainers
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
- Installation
- Quick Start
- Supported Types
- Extended Types
- BSON Integration
- Binary Format
- Root Scope END Byte
- Writing Data
- Reading Data
- Examples
API Reference
- JSYN (Namespace)
- JSYN.Writer
- new Writer(sink, options)
- Properties
- writer.state
- writer.opened
- writer.closed
- writer.failed
- writer.error
- writer.writable
- writer.storable
- writer.depth
- writer.path
- writer.scope
- writer.written
- writer.staged
- writer.hooks
- Writing Methods
- writer.end(...values)
- writer.key(key)
- writer.entry(key, value)
- writer.value(value)
- writer.push(...values)
- writer.add(value)
- writer.ext(typeid, payload)
- writer.object()
- writer.map()
- writer.array()
- writer.set()
- writer.stream()
- writer.text()
- writer.values(values)
- writer.entries(entries)
- writer.assign(source)
- writer.load(source)
- writer.file(path)
- Sink Methods
- writer.to(sink, options)
- writer.pipe(sink, options)
- writer.wipe()
- writer.store()
- Lifecycle Methods
- writer.reset(force)
- writer.abort(error)
- writer.close(end)
- writer.sync()
- writer.flush()
- Output Methods (for store sink)
- writer.release(end)
- writer.buffer(end)
- writer.blob(end)
- Hooks
- writer.hook(hook)
- writer.unhook(hook)
- Stats
- writer.stat()
- JSYN.Reader
- new Reader(source)
- Properties
- reader.state
- reader.opened
- reader.closed
- reader.failed
- reader.error
- reader.depth
- reader.path
- reader.scope
- reader.finished
- reader.paused
- reader.received
- reader.unread
- reader.reserve
- reader.exts
- Reading Methods
- reader.each(fn)
- reader.walk(fn, depth)
- reader.keys(end)
- reader.values(end)
- reader.entries(end)
- reader.flat(depth)
- reader.flatKeys(depth)
- reader.flatValues(depth)
- reader.flatEntries(depth)
- reader.hasNext()
- reader.raw()
- reader.rawId()
- reader.type()
- reader.kind()
- reader.key()
- reader.value()
- reader.entry()
- reader.skip()
- reader.open()
- reader.dict()
- reader.seq()
- reader.flow()
- reader.object()
- reader.map()
- reader.array()
- reader.set()
- reader.stream()
- reader.text()
- reader.end()
- reader.rest()
- reader.save(sink, options)
- Lifecycle Methods
- reader.close()
- reader.reset()
- reader.abort(error)
- Source Methods
- reader.push(...chunks)
- reader.finish(...chunks)
- Extended Types Methods
- reader.register(typeid, handler)
- reader.registerAll(...args)
- reader.unregister(typeid)
- Stats
- reader.stat()
- JSYN.Value
- JSYN.HookList
- JSYN.TypeMap
- JSYN.types
- JSYN.conf
- conf.MAX_BLOCK_SIZE
- conf.MAX_EXTENDED_TYPE_ID
- conf.READER_CACHE_SIZE
- conf.WRITER_BUFFER_SIZE
👩🏽💻Installation
Installing
Install JSyn via npm:
npm i jsynMain 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); // BufferReading 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
1toJSYN.MAX_EXTENDED_TYPE_ID = 268435455 (2^28 - 1). - Value
0is 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:
- Hooks applied to system values are not applied to payload or its nested data.
- 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 classImplicit Interpretation of Data as Extended Type
To interpret a custom class (or other type) as an extended type during writing:
- Implement toJSYN() for objects:
class MyData {
toJSYN() {
return JSYN.ext(1234, "payload");
}
}
writer.value(new MyData());- 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.Valueclass.
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); // ObjectIdNotes
writer.hook(bson.Hook)automatically wraps supported BSON types inJSYN.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>)* EOFVALUE 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/scopeType 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 = 0x16Variable-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 identifierDATA 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 endNotes / 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.
VARINTencodes 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
- Use the
writer.close(end)method withend = true. If writing is already finished, this has no effect. - In storable mode,
writer.release(end),writer.blob(end), andwriter.buffer(end)accept theendargument. - Important:
writer.end(...values)never emits END-ROOT. To include END-ROOT with values:
writer.push(v1, v2, v3);
await writer.close(true); // emits END-ROOTReader 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
Opened – default state after creation.
- Writable = true – actively writing to a sink; all write methods available.
- Writable = false –
close()orend()(from root scope) has been called; all write methods throw errors exceptwriter.end().
Closed – writing has successfully finished.
- All write methods throw errors except
writer.end(). - Can be reset to
openedviawriter.reset()or writing methods that reopen the writer.
- All write methods throw errors except
Failed – fatal error occurred; writing cannot continue.
- Error details available in
writer.error. - Can be reset to
openedviawriter.reset()or reopening methods. - Most operations throw errors except
writer.end().
- Error details available in
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:
writer.to(sink)– send data to a new sink and continue writing (storable mode).writer.reset(true)– clear written data if possible (storable mode).await writer.close()– transitions to closed.writer.end()from root-scope – transitions to closed when the sink finishes writing remaining data.writer.abort()– transitions to failed.- Any fatal error automatically moves writer to failed.
Not allowed / will throw non-fatal error:
Closed State Operations
From closed, the following actions are possible:
writer.reset()– transitions back to opened.writer.pipe(sink),writer.store(),writer.wipe()– transitions back to opened.writer.to(sink)– in storable mode, sends accumulated data to new sink but stays closed.writer.close(),writer.abort()– have no effect.- All write methods throw errors except
writer.end().
Failed State Operations
From failed, the following actions are possible:
writer.reset()– transitions back to opened.writer.pipe(sink),writer.store(),writer.wipe()– transitions back to opened.writer.to(sink)– will throw an error.await writer.sync()– will throw an error.await writer.close()– will throw an error.writer.abort()– have no effect.- All write methods throw errors except
writer.end().
Notes
- The writable flag is automatically managed:
truewhen actively writing,falseafterclose()orend()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:
writer.value(value),writer.values(values),writer.push(...values),writer.ext(typeid, payload)— write values.writer.object(),writer.map(),writer.array(),writer.set(),writer.stream(),writer.text()— open nested scopes.writer.end(...values)— finishes the scope and completes the entire writing process (records values before ending).
2. Array Scope
- Writes a sequence of values (array elements).
- Unsupported values are replaced with
null. - Opened via
writer.array().
Allowed methods:
writer.value(value),writer.values(values),writer.push(...values),writer.ext(typeid, payload)— write values.writer.object(),writer.map(),writer.array(),writer.set(),writer.stream(),writer.text()— open nested scopes.writer.end(...values)— closes the scope (records values before ending).
3. Set Scope
- Writes a sequence of unique values.
- Unsupported values are ignored.
- Opened via
writer.set().
Allowed methods:
writer.value(value),writer.values(values),writer.push(...values),writer.ext(typeid, payload),writer.add(value)— write values.writer.object(),writer.map(),writer.array(),writer.set(),writer.stream(),writer.text()— open nested scopes.writer.end(...values)— closes the scope (records values before ending).
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:
writer.key(key)— write key.writer.entry(key, value)— writes the entry.writer.entries(entries)— writes the entries.writer.end()— close the scope.
When expecting value:
writer.key(key)— overwrites the last key.writer.entry(key, value)— writes the entry.writer.entries(entries)— writes the entries.writer.assign(source)— writes the entries from objectwriter.value(value),writer.ext(typeid, payload)— write value.writer.object(),writer.map(),writer.array(),writer.set(),writer.stream(),writer.text()— open nested scopes.writer.end()— closes the scope, discarding the last key.
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:
writer.key(key)— write key.writer.entry(key, value)— writes the entry.writer.entries(entries)— writes the entries.writer.end()— close the scope.
When expecting value:
writer.key(key)— overwrites the last key.writer.entry(key, value)— writes the entry.writer.entries(entries)— writes the entries.writer.assign(source)— writes the entries from objectwriter.value(value),writer.ext(typeid, payload)— write value.writer.object(),writer.map(),writer.array(),writer.set(),writer.stream(),writer.text()— open nested scopes.writer.end()— closes the scope, discarding the last key.
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:
writer.value(chunk),writer.values(chunks),writer.push(...chunks)— write chunks.writer.load(source)— write chunks from streamwriter.file(path)— write chunks from filewriter.end(...chunks)— closes the scope (records chunks before ending).
7. Text Scope
- Writes a sequence of string chunks.
- Non-string values cause a non-fatal error.
- Opened via
writer.text().
Allowed methods:
writer.value(chunk),writer.values(chunks),writer.push(...chunks)— write chunks.writer.load(source)— write chunks from streamwriter.file(path)— write chunks from filewriter.end(...chunks)— closes the scope (records chunks before ending).
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)
- Description: Data is saved in an internal buffer.
- State: Initial state or after calling
writer.store(). - Properties:
writer.storableistrue. - Reset:
writer.reset(true)can be used to clear stored data and restart writing.
Data extraction methods:
writer.release()— Returns an array of chunks.writer.buffer()— Returns a single concatenated buffer.writer.blob()— Returns aBlob.writer.to(sink)— Redirects accumulated data to another sink and switches to pipe mode.
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 scopevalue– 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 keysroot,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 usingwriter.value(),writer.push(),writer.add(),writer.entry()
Hook Execution Algorithm
For each value being written:
Check for
toJSYN: If the value is an object with atoJSYNproperty that is a function, it is called as:value.toJSYN(key)where key is the current key in scope.
Apply registered hooks: If multiple hooks are registered, the output of the first hook is passed as input to the next, in sequence.
Filter unsupported values: If after hooks the value is one of
undefined,symbol, orfunction(unsupported types), it is handled as follows:
object/map— the key-value pair is ignoredroot/set— the value is ignoredarray— replaced withnull
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
Opened – default state; reading is active.
- All read methods are available.
reader.close()→ transitions to closed.reader.end()orreader.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 (
ENDmarker or EOF) does not change the state; read methods simply returnundefinedorfalse.
Closed – client-initiated completion of reading.
- Read methods return
undefined. reader.reset()→ transitions to opened.reader.close(),reader.abort(),reader.end(),reader.rest()→ have no effect.
- Read methods return
Failed – fatal error occurred; further reading is impossible.
- All read methods throw errors.
- Error can be retrieved via
reader.error. reader.reset()→ transitions to opened.reader.close(),reader.abort()→ have no effect.
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
reader.hasNext()
Checks if another value exists in the current scope.reader.key()
Reads the current key; repeated calls return the last read key.reader.value()
Reads the current value and moves to the next entry.reader.entry()
Reads the current key–value pair and moves to the next entry.reader.skip()
Skips the current entry without returning it.
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. Returnsnumberif a new scope was opened, otherwise0.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
reader.dict()— Opens a dictionary container (covers object/map).reader.seq()— Opens a sequence container (covers array/set/stream/text).reader.flow()— Opens a flow container for sequential reading (covers stream/text).reader.object()— Opens an object container.reader.map()— Opens a map-like container.reader.array()— Opens an array container.reader.set()— Opens a unique-value set container.reader.stream()— Opens a binary stream container.reader.text()— Opens a text container.
Bulk Reading
reader.save(sink, options)
Streams the current container’s remaining data to a sink, closing the scope when done.reader.each(fn)
Appliesfnto each value and closes the container when done.reader.walk(fn, depth)
Appliesfnto each value recursively up todepthand closes all traversed scopes.reader.keys(end)
Returns an async iterator over keys in the current container.
Ifendistrue(default), closes the scope after iteration.reader.values(end)
Returns an async iterator over values in the current container.
Ifendistrue(default), closes the scope after iteration.reader.entries(end)
Returns an async iterator over key–value entries in the current container.
Ifendistrue(default), closes the scope after iteration.
Flattening Utilities
reader.flat(depth)
Returns an async iterator over all values recursively.depthlimits the nested depth to traverse.reader.flatKeys(depth)
Returns an async iterator over keys recursively.depthlimits the nested depth to traverse.reader.flatValues(depth)
Returns an async iterator over values recursively.depthlimits the nested depth to traverse.reader.flatEntries(depth)
Returns an async iterator over key–value entries recursively.depthlimits the nested depth to traverse.
Return Behavior
If there are no more values in the current scope:
reader.open()andreader.hasNext()returnfalse.reader.key(),reader.value(),reader.entry(),reader.skip()returnundefined. If reading has fully finished (no active scopes left), all methods returnundefined.
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 JavaScriptSet, which uses values as keys). - Values: any supported values.
rest()— returns aSet.
4. Object
- Keys: strings.
- Values: any supported values.
rest()— returns an object.
5. Map
- Keys: any supported values.
- Values: any supported values.
rest()— returns aMap.
6. Stream
- Keys: numeric, sequential starting from
0. - Values: binary chunks (
Buffer). rest()— returns a concatenatedBuffer.
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 callingfinish(), further calls topush()orfinish()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()orreader.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