double-meh
v1.2.0
Published
A modern fetch-native HTTP I/O library for browsers and CLIs: caching, request dedup, retry, streaming, SSE, and transparent bundling. Zero dependencies.
Maintainers
Readme
double-meh :// 
double-meh is a modern, fetch-native HTTP I/O library for browsers and CLIs (Node, Bun,
Deno) — a thin, DX-first layer over fetch(). Spend the effort on setup (inspectors, services,
defaults) so that use stays trivial: const person = await io.get(url). REST correctness —
conditional writes, idempotency, problem+json, content negotiation — is a per-call one-liner, not
a project.
The name is the :// symbol — Alex Sexton's "walrus", a.k.a. a "double meh".
Why it might be for you:
- The method declares the return shape.
io.get(url)→ parsed data;io.full.get(url)→ the full response envelope;io.stream.get(url)→ aReadableStream. Options tune behavior, never the return type — noresolveWithFullResponse-style flags. - One envelope contract.
{data, status, ok, headers, response}plus lazily-parsed validators,Linkpagination,Retry-After,Server-Timing— and a thrownBadStatuscarries the same shape, so error handling reads like success handling. - Composable services. In-flight dedup and an app-governed cache (both on by default for GETs), verb-safety-aware retry, and a mock service that runs through the real pipeline — your tests need no server, and a mocked 503 really gets retried.
- Web-native streaming, both directions. Response streams, request streams, and
{writable, readable, response}duplexes that drop straight into a stream-chain pipeline — plus parsed record iteration (JSONL /json-seq) and a reconnecting SSE client on top. - Solid. Zero dependencies, ESM, bundled TypeScript typings, tested across Node, Bun, and Deno.
Examples
The everyday path — and the envelope when you need metadata:
import io from 'double-meh';
const person = await io.get('https://api.example.com/people/42'); // parsed data
const {data, etag} = await io.full.get('https://api.example.com/people/42');Everyday writes are just as terse — objects go out as JSON:
const created = await io.post('https://api.example.com/people', {name: 'Ann'});
await io.put('https://api.example.com/people/42', {...person, email});
await io.patch('https://api.example.com/people/42', {email}); // JSON merge-patch et al. via `as`
await io.del('https://api.example.com/people/42'); // io.delete / io.remove — same verbSafe writes — the library lowers intent to the correct headers and refuses unsafe retries:
// conditional update: If-Match from the read above; a lost race is a 412, not a lost write
await io.put('https://api.example.com/people/42', {...data, email}, {ifMatch: etag});
// or the whole read → apply → conditional PUT loop, with 412 → re-read → retry built in
await io.update('https://api.example.com/people/42', person => ({...person, email}));
// effectively-once POST: one Idempotency-Key minted per logical op, reused across retries
await io.post('https://api.example.com/orders', order, {idempotencyKey: true, retry: true});Failures carry the envelope — problem+json is already parsed:
try {
await io.get('https://api.example.com/missing');
} catch (error) {
if (error instanceof io.BadStatus) console.error(error.status, error.data?.detail);
}Stream a request body up and the response back down through one duplex:
const {writable, readable, response} = io.stream.put('https://api.example.com/bulk', {as: 'jsonl'});
source.pipeTo(writable); // request streams up
const envelope = await response; // status/headers arrive before the body drainsThe lay of the land
- Verbs:
get,head,post,put,patch,delete(+del/remove),options— on the callableio, mirrored underio.full(envelopes) andio.stream(streams/duplexes). A reusable options bag makes an endpoint descriptor shared across verbs. - Options lower intent:
ifMatch,ifNoneMatch,idempotencyKey,accept,as,decode,timeout,retry,cache,track,bust,fields/sort/expandquery builders — the full reference. - Services: track (dedup +
adopt), cache (TTL, 304 revalidation, pattern invalidation), retry (incl. polling), mock. Scope defaults per host with predicates, or isolate consumers entirely withio.create(). - Code-forward: an inline
<head>prelude fires requests before the library loads; the library adopts them seamlessly — Concepts: code-forward. - Extensible everywhere: transports, request/response inspectors (URL-scoped), data & MIME processors, lifecycle events.
Install
npm i double-mehESM-only. CJS consumers can require('double-meh') on Node ≥ 20.19 (require(esm)).
Documentation
The canonical documentation lives in the project wiki — guides, concepts, cookbooks, and the per-module reference, with ranked search.
Migrating from heya/io / heya/io-node? double-meh is their fetch-native successor — the
feature parity map is in the wiki:
Heya-io parity.
Release history
- 1.2.0 Streamed bodies over the
swtransport, a malformed SSEretry:no longer storms reconnects, and{transport: 'sw'}resolves relative URLs against the page. - 1.1.1
io.paginateno longer loops forever when a server'soffsetfails to advance. - 1.1.0 Streamed bundles:
io.bundle.streamingresolves each waiter as its part arrives. - 1.0.0 The initial release.
See the release notes for the long-form history.
License
BSD-3-Clause © Eugene Lazutkin
