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

rdfjs-jelly

v0.1.4

Published

Jelly-RDF parser and writer for RDF/JS in Node.js and browsers

Readme

rdfjs-jelly

Jelly-RDF binary parser and writer for RDF/JS, for Node.js and browsers.

Support

  • Reads Jelly protocol versions 1 and 2.
  • Writes Jelly protocol version 2 only.
  • Supports physical triple, quad, and graph streams; delimited and non-delimited encoding; lookup/repeated-term compression; namespaces; and message metadata.
  • Uses RDF/JS quads and accepts a custom RDF/JS data factory.
  • Does not support RDF-star, generalized RDF, Jelly-Patch, or gRPC.

Node.js 24 or newer is required by the Node build. The browser bundle targets ES2020.

Install

npm install rdfjs-jelly

Command line

Use the package without installing it to inspect a Jelly stream or convert an RDF 1.2 Message Stream Log:

npx rdfjs-jelly inspect 'https://example.org/data.jelly.zst'
npx rdfjs-jelly convert messages.log --output messages.jelly.zst

Both commands accept a URL, local file path, or - for stdin. Plain, gzip, and zstd-compressed input is detected from its bytes. read is an alias for inspect; run npx rdfjs-jelly --help for the complete usage. Conversion is a bounded-memory pipeline: the input, RDF Message parser, Jelly writer, native zstd compressor, and output file are all streamed.

Parse and write

import { DataFactory, Parser, Writer } from 'rdfjs-jelly';

const { literal, namedNode, quad } = DataFactory;
const writer = new Writer({ namespaces: { ex: 'https://example.org/' } });

writer.addQuad(quad(
  namedNode('https://example.org/s'),
  namedNode('https://example.org/p'),
  literal('hello', 'en'),
));

writer.end((error, bytes) => {
  if (error) throw error;
  const quads = new Parser().parse(bytes!);
  console.log(quads);
});

Writer emits a delimited version-2 stream by default. Set delimited: false for a single-message protobuf payload. A non-delimited output cannot contain multiple messages.

Messages

One Jelly RdfStreamFrame is exposed as one Message. Message extends Array<RDF.Quad> and carries messageCounter and binary metadata.

const writer = new Writer();
writer.addMessage([quad1], { source: new TextEncoder().encode('sensor-a') });
writer.addMessage([]); // Empty messages are preserved.
writer.addMessage([quad2]);

writer.end((error, bytes) => {
  if (error) throw error;
  const messages = new Parser().parseMessages(bytes!);
  console.log(messages.map(message => message.messageCounter));
});

For compatibility with rdf-parser-ts, use new Parser({ messages: true }) to receive { quad, messageCounter } entries. The returned array has a messageCount property. isMessageQuad() and toMessages() convert between flat and grouped forms while preserving empty messages.

Node streams

import { createReadStream } from 'node:fs';
import { StreamParser } from 'rdfjs-jelly';

for await (const quad of createReadStream('data.jelly').pipe(new StreamParser())) {
  console.log(quad);
}

StreamWriter accepts RDF/JS quads in object mode and emits binary chunks. Both Node stream classes expose import(readable).

Browser streams

import { StreamParser } from 'rdfjs-jelly/browser';

const response = await fetch('/data.jelly');
for await (const quad of response.body!.pipeThrough(new StreamParser())) {
  console.log(quad);
}

The browser parser accepts Uint8Array and ArrayBuffer chunks. Browser and Node parsers emit options, namespace, message, and messageCounter events.

Browser playground

Build the browser bundle, serve the repository, and open /index.html:

npm run build:browser
python3 -m http.server 8000

The bundled example/osm-dk-10k.jelly.gz dataset is selected by default. The playground accepts URLs and uploaded files, and detects plain, gzip, and zstd input from its magic bytes rather than the filename. It steps through one Jelly RDF Message at a time, can fast-forward to the end, and retains a rolling history of 20 messages.

The conversion mode parses an RDF 1.2 Message Stream Log with rdf-parser-ts, preserves empty message boundaries, writes Jelly protocol version 2, compresses the result as a standard zstd frame, and saves a .jelly.zst file. Conversion streams through the parser, Jelly encoder, and zstd WASM codec into a file chosen with the File System Access API. Browsers without that API cannot provide a bounded-memory download and should use the CLI. Browser zstd input is streamed when the browser exposes a native zstd DecompressionStream; otherwise the WASM fallback still requires a complete compressed input buffer. Gzip and uncompressed input remain streaming.

Compression

Transport compression is intentionally separate from Jelly framing. Node.js 22.15/23.8 and newer expose createZstdCompress() and createZstdDecompress() in node:zlib, alongside gzip and Brotli transforms:

import { createReadStream } from 'node:fs';
import { createZstdDecompress } from 'node:zlib';
import { StreamParser } from 'rdfjs-jelly';

const quads = createReadStream('data.jelly.zst')
  .pipe(createZstdDecompress())
  .pipe(new StreamParser());

Browser CompressionStream and DecompressionStream do not currently expose zstd. The playground therefore uses @hpcc-js/wasm-zstd for incremental zstd output compression and buffered zstd input decompression, while retaining the native browser stream API for gzip. The WASM codec is isolated in dist/browser/playground.mjs, so it does not increase the normal dist/browser/index.mjs parser bundle.

Performance

These are local microbenchmarks, not universal rankings. They measure 100,000 generated RDF triples with unique subjects and literals and one repeated predicate. Each result is the median of 15 measured runs after two warm-up runs, with case order alternated between rounds. Parsing includes constructing the result RDF objects. Writing includes constructing the writer, adding all quads, and final serialization, starting with already constructed objects. Compressed parsing includes synchronous decompression and uses Node.js's default gzip and Brotli settings; compression itself is performed before the timed section.

Snapshot recorded on 2026-06-29 with Node.js 25.9.0 and Python 3.12 on Linux, using an Intel Core i7-1265U and 30 GiB RAM.

Compared with rdf-parser.ts

This compares equivalent RDF data, but not identical input formats: rdfjs-jelly parses Jelly while rdf-parser.ts 0.2.6 (ce8846b) parses N-Triples. It therefore reflects the end-user format choice as well as parser implementation performance.

Parsing

| Parser | Format | Input size | Median | Throughput | | --- | --- | ---: | ---: | ---: | | rdfjs-jelly | Jelly | 2,579,568 B | 42.9 ms | 2.33 M statements/s | | rdf-parser.ts | N-Triples | 6,377,780 B | 46.6 ms | 2.15 M statements/s | | rdfjs-jelly | Jelly + gzip | 496,581 B | 45.7 ms | 2.19 M statements/s | | rdf-parser.ts | N-Triples + gzip | 490,694 B | 47.0 ms | 2.13 M statements/s | | rdfjs-jelly | Jelly + Brotli | 128,277 B | 44.6 ms | 2.24 M statements/s | | rdf-parser.ts | N-Triples + Brotli | 117,438 B | 46.9 ms | 2.13 M statements/s |

Parsing performance is within about 9% for all three transport variants on this dataset. Uncompressed Jelly was about 60% smaller (2.47 times less data). The highly repetitive N-Triples input was about 1.2% smaller after gzip and 8.4% smaller after Brotli. Compressed sizes are dataset-dependent and should not be extrapolated to less repetitive RDF.

Writing

| Writer | Format | Output size | Median | Throughput | | --- | --- | ---: | ---: | ---: | | rdfjs-jelly | Jelly | 2,579,568 B | 126.6 ms | 0.79 M statements/s | | rdf-parser.ts | N-Triples | 6,377,780 B | 35.3 ms | 2.83 M statements/s |

For this workload, rdf-parser.ts wrote N-Triples about 3.6 times faster. Jelly writing performs lookup-table management, frame construction, and protobuf serialization, producing an output about 60% smaller before transport compression.

Profiling and optimization

The initial V8 profile spent about 337 ms of a 376 ms parse in protobuf framing and decoding. Its hottest functions were Protobuf-ES descriptor lookup and reflective message construction, followed by garbage collection. RDF/JS materialization itself took about 38 ms.

The optimized path uses schema-specific static protobuf.js code with monomorphic decoder call sites. Common triple, IRI, literal, and lookup-entry payloads are decoded directly from protobuf bytes into the RDF/JS factory, avoiding temporary protobuf objects and frame/row object graphs. Plain Parser.parse() also writes quads directly to its result array instead of constructing intermediate Message arrays, and Node inputs are normalized to zero-copy Buffer views.

Immutable datatype terms are cached by their bounded Jelly lookup IDs. A generic string-keyed NamedNode cache was tested but rejected: on this unique-subject workload it increased parse time because Jelly's repeated-term encoding already handles the common reuse case. Together with straight-line statement validation, the retained changes reduced the complete parse from about 359 ms at the measured pre-optimization baseline to roughly 37–43 ms, or about 9 times faster.

The writer profile originally spent about 137 ms maintaining lookup tables and constructing frame rows, plus about 72 ms converting and serializing protobuf objects. Lookup eviction now uses indexed O(1) LRU links instead of repeatedly creating Map iterators, lookup insertion and reference selection share one map probe, and statement encoding reuses scratch storage with straight-line term comparisons. Frames are serialized directly from Jelly rows without first creating a second protobuf object graph. In the comparison above these changes reduced writing from 200.6 ms to 126.6 ms, about 1.6 times faster.

Reproduce the phase breakdown with:

npm run perf:profile -- 100000 7
npm run perf:profile:writer -- 100000 7

Reproduce it from this repository, with ../rdf-parser.ts built:

npm run perf:compare:rdf-parser -- 100000 15

Set RDF_PARSER_TS_PATH to compare against a different build.

Development

npm install
npm run lint
npm test
npm run build
npm run check
npm run proto:generate

The checked-in schema is Jelly-RDF rdf.proto 1.1.1. Static JavaScript codecs and TypeScript declarations are generated with protobuf.js. The generation step replaces generic oneof setter calls with cheap discriminator markers; src/generated/rdf_pb.ts consumes those markers while preserving protobuf's last-one-wins semantics.

Tests include pinned official Jelly conformance fixtures and pyjelly-compatible version-2 writer behavior.

License

Apache 2