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

flowdoc-napi

v1.1.4

Published

Fast multilanguage serialization format - Node.js native (N-API) binding

Readme

flowdoc-napi

Node.js binding (via napi-rs, a native N-API addon — not WASM) for FlowDoc — a fast serialization format: indent-delimited key: value records, parsed by a shared Rust core (flowdoc-core) linked directly into Node as a native addon.

const native = require('flowdoc-napi'); // or require('.') from inside this package

const records = JSON.parse(native.parseFlowJson(`
Record
  id: 1
  name: Test
`));
// [{ id: '1', name: 'Test' }]

This is a second Node.js binding, alongside bindings/nodejs (wasm-bindgen). It exists to answer one question directly: is Node's WASM boundary the reason FlowDoc trails native JSON.parse, or is it something else? A diagnostic echo function (below) isolates the answer.

Entry points

  • parseFlowJson(data) — returns a JSON string. Use this one.
  • parseFlowDirect(data) — builds napi Array/Object values directly during the parse (no intermediate Rust HashMap, no JSON string) — the same technique that made bindings/python's parse_flow ~2x faster. Real improvement over parseFlow below, but lands tied with parseFlowJson, not ahead of it (see below) — kept as a correct, documented alternative, not the recommendation.
  • parseFlowBinary(data) — returns a Buffer in the same length-prefixed binary wire format the FFI bindings use (flowdoc_parse_binary), for the caller to decode in JS with decode_binary.js's decodeBinary(). Measured slower than parseFlowJson for the full round trip, even though the Rust-side encode alone is faster than JSON encoding — see below for why.
  • parseFlow(data) — returns parsed records as native JS objects by converting a Rust HashMap<String, String> through napi's generic conversion. Measured slower than all of the above — kept as a documented negative result, not removed.
  • echo(data) — diagnostic only: returns its input unchanged, doing no parsing. Isolates N-API call/marshaling overhead from the real work.

What was actually measured (1000-record fixture, this repo's shared benchmark)

| Path | Time | vs. native JSON.parse | |---|---:|---| | echo (N-API call overhead only) | ~0.03ms | — | | parseFlow (HashMap + generic conversion) | ~0.69-0.74ms | ~5.3-5.7x slower | | parseFlowBinary + decodeBinary() (JS-side decode loop) | ~0.64-0.70ms | ~4.9-5.4x slower | | parseFlowDirect (build during parse, no HashMap) | ~0.56-0.59ms | ~2.7-3.0x slower | | parseFlowJson + JSON.parse | ~0.57-0.62ms | ~2.7-3.0x slower | | bindings/nodejs's WASM binding, for comparison | ~0.71ms | ~3.6x slower | | native JSON.parse | ~0.20ms | — |

parseFlowDirect was the obvious next experiment after mem::take — Python's parse_flow got a real ~2x win from building PyDict/PyList objects directly during the parse instead of building a Rust HashMap first and converting it afterward. The same change here (parseFlowDirect, built with napi::Env::create_object/create_array, Object::set) does show a real, reproducible win over the old parseFlow (~0.58ms vs. ~0.69-0.74ms — removing the intermediate HashMap and its generic napi conversion pass genuinely helps). But it does not beat parseFlowJson — the two are statistically tied across repeated runs. Python's win came specifically from skipping PyO3's generic-conversion overhead; here, the remaining cost is dominated by making ~3000 individual N-API property-set calls (one native call per field — each one also allocates a CString for the key internally, since N-API's named-property functions require a NUL-terminated C string), and that per-call cost turns out to be about the same as one Rust-side JSON encode plus one native JSON.parse() call. parseFlowJson remains the recommended entry point.

Four real findings, all consistent with what's already documented for bindings/nodejs's WASM binding and bindings/php's parseFlowBinary in the main CLAUDE.md:

  1. N-API's own call overhead is negligible (~0.03ms, from echo) — the WASM-vs-native-addon question was never really about FFI/binding overhead in the first place.
  2. Returning a native JS object directly is slower than returning a JSON string and calling JSON.parse(), even after removing every avoidable Rust-side allocation. parseFlow (build a HashMap, convert it generically) and parseFlowDirect (skip the HashMap, build the napi object directly, the technique that won ~2x for Python) both lose to parseFlowJson; parseFlowDirect closes most of that gap but not all of it. The common cost neither version can avoid is ~3000 individual N-API property-set calls (one native call per field) — that per-call cost is roughly equal to one Rust-side JSON encode plus one native JSON.parse() call.
  3. Eliminating N-API calls entirely doesn't help either. parseFlowBinary + decodeBinary() moves the per-field work to a pure JS loop over a Buffer/DataView — zero N-API boundary crossings per field, unlike every option above. It still measures slower than parseFlowJson (~0.64-0.70ms vs. ~0.57-0.62ms), even though the Rust-side binary encode alone is faster than the JSON encode. The bottleneck there is Buffer#toString(), called ~2000 times in the decode loop — each call is native and JIT-adjacent, but ~2000 of them still cost more in total than one purpose-built JSON.parse() call. Four attempts now (parse_flow_wasm, parseFlow, parseFlowDirect, parseFlowBinary+decodeBinary), across three different boundary technologies (WASM marshaling, N-API calls, a pure-JS decode loop), and the pattern holds every time: any shape that does real per-field work loses to batching into one call. This isn't a boundary-technology problem — it's that FlowDoc's per-field data (2-3 short strings per record) is exactly the shape JSON's parser was built to consume in bulk, and nothing tried here changes that shape.
  4. The mem::take core fix and the WASM→N-API switch are both real, and they stack — parseFlowJson averages ~0.58ms versus the WASM binding's ~0.71ms on identical input, a reproducible ~18-20% improvement, closing the JSON gap from ~3.6x down to ~2.7-3.0x.

It is not enough to beat native JSON.parse, though. The diagnostic breakdown says why: the ~0.55ms of real cost (total minus echo's ~0.03ms overhead) is genuine FlowDoc-parse-plus-JSON-encode work in Rust, competing against a JSON parser that is one of the most heavily optimized code paths in V8 — and the one technique that reliably beats generic per-field marshaling (batching into one string, one parse call) is already what parseFlowJson does. Closing the remaining gap would need the Rust-side parse-plus-encode step itself to get faster in absolute terms — e.g. avoiding the per-field String allocation in flowdoc_core::Record itself (would affect every binding, not just this one) — not attempted here, and not guaranteed to be enough on its own even if it works: mem::take was a ~30-40% win at the Rust level and only translated to ~18-20% here once JS-side costs were included, so a further Rust-only win would likely translate to something smaller still once the JS side dilutes it further.

Licensing (soft gate)

parseFlowJson/parseFlow/parseFlowDirect/parseFlowBinary work identically whether or not a license key is configured — there is no Pro-exclusive capability gated by this yet. If FLOWDOC_LICENSE_KEY is set, this package validates it once per process against FLOWDOC_LICENSE_SERVER + /api/licenses/validate (no default server — validation is skipped entirely if this isn't set too), asynchronously, and logs a warning on an invalid key or an unreachable server.

This lives in a separate hand-written file, license.js, not the napi-rs-generated index.js — it's pure HTTP client logic with no native/Rust involvement, so requiring it doesn't need the compiled addon at all. Import it via the subpath:

const { licenseStatus } = require('flowdoc-napi/license');

const status = await licenseStatus();
// { checked: true, valid: true | false | null, error: string | null }

valid is null when there was nothing to check (no key configured) or nothing could be checked (no server configured, or unreachable) — see license.js for the full behavior.

In production, set FLOWDOC_LICENSE_SERVER=https://license-admin.sendwavehub.tech/api (the trailing /api is required — see RELEASING.md's "Production license server" section for why). There is no default; validation is skipped entirely without it.

Activation

activateLicense(activatedBy, options) is a separate, explicit call — unlike licenseStatus(), it never runs automatically, since it's a mutating call (it flips the license to "Activated" server-side, unlike /validate's read-only check). Call it once, e.g. on first run/install:

const { activateLicense } = require('flowdoc-napi/license');

const result = await activateLicense('install-script', { activationIp: '1.2.3.4', metadata: { os: 'linux' } });
// { success: true, error: null, message, tier, seats, expiresAt, customerId, signedLicenseArtifact }
// or, on failure: { success: false, error: string, ...other fields null }

Posts to FLOWDOC_LICENSE_SERVER + /licenses/<FLOWDOC_LICENSE_KEY>/activate (a single /api/ segment, since FLOWDOC_LICENSE_SERVER is expected to already carry one — see /validate's doubled /api/api/ above). options.activationIp/options.metadata are both optional. Cache signedLicenseArtifact yourself if you need it later; this function doesn't persist anything.

Build & test

cd bindings/nodejs-napi
npm install
npm run build   # napi build --platform --release -- also (re)generates index.js/index.d.ts
npm test        # runs tests/test.js then tests/test_license.js
npm run bench

npm install installs @napi-rs/cli (a devDependency) — the build script uses its napi build command instead of a hand-rolled cargo build + cp, so it produces a correctly platform-named addon (flowdoc-napi.<platform>-<arch>[-<abi>].node) automatically, on whatever OS/arch it runs on, rather than assuming macOS the way an earlier prototype version of this binding did.

index.js/index.d.ts (checked into git, unlike the compiled *.node addon which is gitignored) are generated by napi build and loaded via require('flowdoc-napi') — they detect the running platform/arch and require() either a same-directory .node file (a local build, or a monorepo checkout) or one of three optional per-platform npm packages (flowdoc-napi-darwin-arm64, flowdoc-napi-linux-x64-gnu, flowdoc-napi-win32-x64-msvc, each just a compiled addon — see npm/) that npm install picks based on the installing machine's platform. Those three, plus Windows/Linux/macOS(arm64) native .node builds, are what .github/workflows/build-nodejs-napi.yml builds per-platform and publishes to npm on a vX.Y.Z tag — see that workflow for the exact napi-rs (napi prepublish) publish sequence.

See the FlowDoc project for the format overview, benchmark numbers, and links to every other binding.