Skip to content

Heya io parity

Eugene Lazutkin edited this page Jul 10, 2026 · 1 revision

double-meh vs heya/io + heya/io-node — feature parity

Survey date: 2026-06-30, markers refreshed 2026-07-09. Goal: confirm double-meh is a superset of heya/io (1.9.3) + heya/io-node (1.3.0), modulo features deliberately dropped. Sources: the two repos' source + the heya/io GitHub wiki (12 cookbooks + API refs); mapped against double-meh's src/. Companion to the design record (§ "Dropped from heya/io").

Legend: ✅ replicated · ➕ replicated + improved (superset) · ❌ decided against (with reason)

Verdict: double-meh is a superset of the heya libraries — pipeline, services, extensibility, streaming, return model — and adds a whole REST-correctness layer heya never had. Every gap the original survey queued has since shipped (bundle, compression, both progress directions, persistent cache backends, retry extras, fetch-init passthrough); what remains absent is intentional: the XHR era (XHR/jsonp/load/node transports, FauxXHR, swappable Deferred).


Core pipeline & return model

heya feature double-meh Status
io(options) callable; string or options-bag io(options) callable; string, URL, or bag
Verbs get/head/post/put/patch/delete/options + del/remove same set + del/remove
verb(url, data) signature verb(url, data?, options?) — adds an override bag
requestHasNoBody / responseHasNoBody readVerbs / noResponseBody / metaVerbs
Return: Result/BadStatus/TimedOut/FailedIO, returnXHR, Ignore envelope (io.full / errors share it), decode-once; FailedIO/BadStatus/TimedOut carry the envelope ➕ (envelope > raw XHR; problem+json on e.data)
io.processOptions (request rewrite hook) request inspectors (io.inspect.request)
io.processSuccess / io.processFailure response inspectors (io.inspect.response) + throw-on-non-2xx
io.buildUrl / makeKey / makeQuery / makeVerb io.buildUrl / io.makeKey / makeVerb (makeKey = canonical sorted-query)
returnXHR (resolve raw XHR) ❌ — io.full returns the envelope; .response is the real Response ❌ raw-XHR concept obsolete
Ignore marker (send data unprocessed) dataProcessor returns the body as-is ✅ (folded into processors)

Net-new (no heya equivalent): conditional writes (ifMatch/ifNoneMatch), idempotencyKey, io.update() (conditional-PUT + 412 conflict-retry), query builders (fields/sort/expand), as content-type shorthand + registry, lazy envelope getters (etag/weak/lastModified/ location/links/contentType/retryAfter/serverTiming), code-forward (adopt/__doubleMeh), an events emitter, scalar query, BadStatus.problem (generic error-envelope parsing).

Transports

heya transport double-meh Status / reason
fetch the one core transport ✅ (plus the opt-in sw message transport — net-new)
Transport registry (io.transports, defaultTransport, registerTransport) same
XHR (xhrTransport) + FauxXHR + XHR-isms (responseType, overrideMimeType/mime, withCredentials shim, user/password, header-string parsing) fetch + real Response/Headers make the emulation layer collapse; new Response() replaces FauxXHR
jsonp transport ❌ obsolete
load (<script>) transport ❌ too exotic now
Node http/https transport fetch() is universal — one transport for browser + CLI

Services / middleware

heya service double-meh Status
cache (priority 50, GET-default, save/remove/clear, swappable storage) io.cache — GET-default, app-governed TTL, 304 revalidation, prefix/regexp/fn invalidation, sweep() ✅ core; ➕ app-governed + Vary-aware keys
— storage backends (sessionStorage/localStorage) persistent backends: Cache API / fs / node:sqlite (+ in-memory default) behind one storage interface (shipped 2026-07-03; Web-Storage plugs into the same interface)
track (priority 40, dedupe in-flight, wait) io.trackrun-level decoded-envelope dedup (decode-once); track: 'wait' registers interest ➕ (shares the decoded envelope, not a re-readable XHR)
retry (retries, continueRetries, nextDelay, initDelay) io.retry — verb-aware safety, Retry-After, DELETE-404→204, continueRetries predicate + polling mode ➕ (safer, and a superset of the knobs)
mock (exact/prefix/regexp/fn; returns value/promise/XHR/redirect) io.mock — exact/prefix/regexp/fn; returns Response/value; composes with the real pipeline ✅ ➕ (mints real Responses)
bundle (priority 10, request coalescing, heya-bundler server) io.bundle — windows, named bundles + flush, multi-bundler, MIME-keyed unbundle, writeThrough (shipped 2026-07-04; server = double-meh-bundler)
bust (cache-busting query param) bust: true | 'name' — never cached
scaffold (service factory: optIn/attach/detach) io.attach/detach + per-service optIn, priority onion

Extensibility hooks

heya hook double-meh Status
dataProcessors (by constructor) io.dataProcessors / io.registerData
mimeProcessors (by content-type) io.mimeProcessors / io.registerMime ✅ ➕ (decode sees the options)
processData / getData / getHeaders encodeBody / decode / makeEnvelope ✅ (reworked)
Transport & service registration registerTransport / attach+detach
io.Deferred / FauxDeferred (swappable promise) ❌ native Promise everywhere
io.AbortRequest() options.signal (native AbortSignal) ➕ native
Node inspectRequest / inspectResult request/response inspectors (generalized)
Node addEncoder / removeEncoder io.encoders registry + factories + compress: fn escape hatch (shipped 2026-07-03/09)

Events & progress

heya double-meh Status
(no EventEmitter) io.on/off/emit events emitter ➕ net-new
onDownloadProgress onDownloadProgress callback (buffered and streamed reads alike) (shipped 2026-07-02)
onUploadProgress onUploadProgress — streams meter per chunk; buffered = one completion event (shipped 2026-07-09; fetch-bound limits)
onProgress, Deferred.progress both directions share one Progress shape / ❌ no swappable Deferred to progress ✅ / ❌

Streaming (heya/io-node)

heya-io-node double-meh Status
responseType:'$tream' (raw response stream) io.stream.get(url) / {stream:true} → Web ReadableStream ➕ Web-native
heya-io-node/stream IO Duplex + stream verbs (.pipe() in/out) io.stream.put/post/patch{readable, writable, response} Web duplex ➕ Web-native, stream-chain-composable
Request-body streaming (pass Readable/Buffer) pass a Web ReadableStream/Blob/File (body-type-driven) or a {readable} chain
.meta (response FauxXHR), .getData()/.getHeaders() .response promise (envelope: status/headers)
Node Readable/Duplex streams Web streams onlynode:stream deliberately absent ❌ Node-stream code (browser target; stream-chain bridges Web→Node)

Compression (heya/io-node)

heya-io-node double-meh Status
Auto response decompression (content-encoding) fetch decompresses automatically ✅ (via platform)
Request-body compression ($-Content-Encoding, encodingThreshold) compress: true | name | fn — universal gzip/deflate, opt-in br/zstd (shipped 2026-07-03)
io.node.encoders / addEncoder / acceptedEncoding / preferredEncoding io.encoders registry; response side is the platform's
compressor / decompressor (zlib opts) factories + installZlibEncoders(io, {...}) — parameterized at registration (shipped 2026-07-04)

Options — parity table

heya option double-meh Status
url, method, query, data, headers same
signal native AbortSignal
ignoreBadStatus ignoreBadStatus
cache, track, mock (opt-in) same (cache/track default-on for GETs)
retries, nextDelay, initDelay in the retry option bag (defaults on io.retry)
bundle bundle: true | 'name' (shipped 2026-07-04)
wait, bust track: 'wait', bust
continueRetries same (in the retry bag; retries: 0 = polling)
timeout same (options.timeoutTimedOut)
returnXHR, processSuccess/processFailure (per-req) io.full / response inspectors ➕ / ✅
responseType (blob/arraybuffer/document/json/text) decode option + mimeProcessors + {stream:true} ⚙️ reworked — decode: 'blob' | 'arrayBuffer' | … replaces responseType
withCredentials, fetchMode/fetchCache/fetchRedirect/fetchReferrer/fetchReferrerPolicy/fetchCredentials/fetchIntegrity the {fetch} RequestInit sub-bag (closed 2026-06-30 from this survey's findings)
user/password, mime (overrideMimeType) ❌ XHR-isms (use headers/Authorization; content-type via as/headers)
callback (jsonp), responseType:'$tream', $-Content-Encoding, compressor/decompressor jsonp ❌; $treamio.stream ✅➕; compression ✅ ✅ modulo the intentional jsonp drop

Net-new options: ifMatch, ifNoneMatch, idempotencyKey, as, fields, sort, expand, page, compress, decode, stream, onDownloadProgress, onUploadProgress, meta, transport.

Cookbook recipes (heya/io wiki, 12) — can we replicate?

Cookbook double-meh Status
main (verbs, query/headers/returnXHR, error types, FormData, Promise.all) verbs + envelope/errors + FormData passthrough ✅ (io.full replaces returnXHR)
manipulate requests (processOptions: virtual hosts, auth injection) request inspectors rewrite url/headers
manipulate responses (processFailure chain, instanceof FailedIO/BadStatus, 401/403) response inspectors + catch (e) { e instanceof io.BadStatus; e.status; e.data }
bundle io.bundle + the double-meh-bundler server package (shipped 2026-07-04)
cache (attach, localStorage, cache:false, remove exact/wildcard/regexp, clear) invalidation patterns ✅; persistent backends behind one interface ✅ ➕
mock (patterns, redirect, delay, error) io.mock all four matchers; 503→200 gets retried
track (wait:true register interest) dedup ✅; track: 'wait' register interest ✅
bust (bust:true / custom key) same (bust: true / 'name'), never cached
retry (retries:0, continueRetries: r => r.status>=500, delays) all in the retry bag, incl. continueRetries polling
jsonp ❌ dropped
load ❌ dropped
fetch (attach/detach, fetchMode/fetchCredentials/…) fetch is the default; per-request init via the {fetch} sub-bag

Summary — the delta

❌ Decided against (intentional — see the design record § "Dropped from heya/io"): XHR transport + FauxXHR + all XHR-isms (responseType emulation, mime/overrideMimeType, withCredentials shim, user/password, header-string parsing); jsonp and load transports; Node http/https transport; swappable io.Deferred/FauxDeferred + AbortRequest (native Promise/AbortController instead); returnXHR (the envelope instead). Node streams — Web streams only.

✅ Closed since the survey (2026-06-30 → 2026-07-09): fetch-init passthrough and track: 'wait' (from this survey's findings, 2026-06-30/07-02); bust, continueRetries polling, timeout, onDownloadProgress (2026-07-02); persistent cache backends + Vary keying, request compression (2026-07-03); the bundle client + parameterized encoders (2026-07-04); upload progress, the compress: escape hatch, generic error envelopes (BadStatus.problem), and the pluggable-envelope surface (2026-07-09).

➕ Superset (no heya equivalent): conditional writes + idempotency + io.update(); the response envelope with lazy validators + problem+json + BadStatus.problem; query builders (fields/sort/expand) + page; the as content-type registry; code-forward; the events emitter; canonical makeKey; pagination iteration (io.paginate, io.getByIds); record streaming (io.records) + SSE; the Web-native streaming duo (io.stream duplexes) that modernizes heya-io-node's $tream; the Service-Worker integration (hello handshake, sw transport, cross-tab invalidation).

So: double-mehheya/io + heya/io-node on everything that survives the fetch-native redesign — with no remaining queued gaps. What a migrating heya user misses is only what was dropped on purpose, with a modern replacement named for each drop.

Clone this wiki locally