-
Notifications
You must be signed in to change notification settings - Fork 0
Heya io parity
Survey date: 2026-06-30, markers refreshed 2026-07-09. Goal: confirm
double-mehis a superset ofheya/io(1.9.3) +heya/io-node(1.3.0), modulo features deliberately dropped. Sources: the two repos' source + theheya/ioGitHub wiki (12 cookbooks + API refs); mapped againstdouble-meh'ssrc/. 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).
| 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).
| 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 |
| 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.track — run-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 |
✅ |
| 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) |
| 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 |
✅ / ❌ |
| 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 only — node:stream deliberately absent |
❌ Node-stream code (browser target; stream-chain bridges Web→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) |
| 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.timeout → TimedOut) |
✅ |
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 ❌; $tream→io.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 | 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 |
✅ |
❌ 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-meh ⊇ heya/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.
Guides
Concepts
Services
Modules
- fetch transport
- keys & URLs
- helpers
- records
- sse
- storage backends
- compression encoders
- Service Worker integration
Cookbook