Modernizing Steem.js — Day 4: Transports, Runtime Globals & Dropping Bluebird
Part 4 of my 7-part Steem.js modernization series. With signing and serialization now running on modern, edge-safe code, today we make the networking layer and the runtime assumptions portable too.
The problem: hidden Node assumptions
A library can have perfect crypto and still fail to load on an edge runtime because of small things scattered across the code:
- a hard dependency on
cross-fetch - a static
import 'ws'that bundlers pull in even for browsers process.on('warning', …)at module top levelwindow/globalassignment that throws where neither existsbluebirdfor promisification
I went through each.
HTTP transport → native fetch
Every modern runtime — Node 18+, browsers, Cloudflare Workers, Vercel Edge, Deno — ships a global fetch. So src/api/transports/http.js dropped cross-fetch and now uses globalThis.fetch directly (the existing options.fetchMethod override still works if you want to inject your own):
const fetchMethod = this.options.fetchMethod || globalThis.fetch;
const res = await fetchMethod(this.options.uri, {
method: 'POST',
body: JSON.stringify(payload),
});
One less dependency, and it works identically everywhere.
WebSocket transport → lazy, optional
WebSockets are trickier: browsers and Deno have a global WebSocket, but Node needs the ws package. The fix is to resolve the global first and only fall back to a dynamic, lazy import:
// ws.js — never statically bundled
let WS = globalThis.WebSocket;
if (!WS) {
WS = (await import('ws')).default; // optional dependency, Node only
}
ws is now an optionalDependency and marked external in the build — so edge and browser bundles never pull it in, and Node users who want WebSocket transport get it automatically.
Guarding runtime globals
Non-portable globals are now feature-detected instead of assumed:
// src/index.js
if (typeof process !== 'undefined' && process.on) {
process.on('warning', …);
}
The IIFE browser build's window/global assignment is likewise wrapped in a typeof globalThis guard. The library no longer assumes it's in Node, a browser, or anywhere specific.
Goodbye Bluebird, hello native promises
Steem.js's signature feature is its dual callback/promise API — every method works with a trailing cb or returns a Promise. That was powered by Bluebird's Promise.promisify / promisifyAll. Bluebird is pure-JS and isomorphic, so it worked — but it's a large dependency for something native promises now do well.
I replaced it with a tiny in-repo src/_promise.js:
export function promisify(fn) { /* returns a fn that resolves/rejects a native Promise */ }
export function promisifyAll(obj) { /* adds `${name}Async` variants */ }
export function nodeify(promise, cb) { /* bridges promise → callback */ }
export function delay(ms) { /* native setTimeout promise */ }
The api and broadcast modules now generate their fooAsync / fooWithAsync variants from this — so getAccounts(cb), getAccountsAsync(), getAccountsWith({...}) all behave exactly as before, with one fewer dependency.
The net effect
The dependency list is now tiny: @noble/*, @scure/base, bs58, retry, plus optional ws. No cross-fetch, no bluebird, no Node-builtin polyfills. The bundle was audited to contain zero Node-builtin imports and zero dynamic requires — the prerequisite for true edge compatibility.
Tomorrow: Phase 5 — shipping first-class TypeScript types for a dynamically-generated API.
Links
- 🛠️ Code (fork): https://github.com/blazeapps007/steem-js (
BlazeDevelopmentbranch) - 📖 Documentation: https://blazeapps007.github.io/steem-js/
Support Secure Steem Development
If you value proactive engineering, UX polish, and performance optimizations for the STEEM ecosystem, please consider supporting my witness: blaze.apps
🗳️ Vote Here:
Vote for blaze.apps Witness