API Compatibility Assessment
This document assesses the compatibility of qjs-lws's JavaScript wrapper libraries (lib/ and lib/lws/) against their corresponding web standards and Bun.js APIs.
Executive Summary
The implementation demonstrates high conformance to WHATWG standards in most areas, with particularly strong coverage of:
- Streams API: Complete implementation (ported from web-streams-polyfill)
- URL/URLSearchParams: Complete
- WebSocket API: High conformance
- AbortController/AbortSignal: High conformance
However, there are critical incompatibilities in the Fetch API implementation that will break standard-compliant code:
- Response.status is a method, not a property (major spec violation)
- Body.formData() returns a plain object instead of FormData instance
- Headers iteration order uses insertion order instead of sorted order
- fetch() doesn't accept Request objects as input
- AbortSignal handling in fetch() is incomplete
The Bun.js API compatibility is partial, with significant gaps in:
- Server object: 11 of 15 properties/methods missing
- WebSocket handler options: 10 of 13 options missing
- UDP socket: 10 of 12 methods missing
WHATWG Fetch API
Reference: https://fetch.spec.whatwg.org/
Headers (lib/lws/headers.js)
Conformance: High (90%)
Implemented:
- All core methods:
get(),set(),has(),delete(),append() getSetCookie()for Set-Cookie special handling- Iteration:
forEach(),keys(),values(),entries(),[Symbol.iterator]() - Proper name normalization and validation
Incompatibilities:
- Iteration order: Uses insertion order instead of spec-mandated sorted (lexicographic) order
- Guard mode: Not implemented (spec defines immutable/request/response guards)
- Non-standard extension:
toObject()method (Express-style convenience)
Reinventing the wheel: The iteration order deviation is unnecessary and breaks spec-compliant code that depends on sorted header ordering.
Body Mixin (lib/lws/body.js)
Conformance: Medium (75%)
Implemented:
text(),json(),arrayBuffer(),blob()- all return correct typesbodyproperty as ReadableStreambodyUsedflag
Incompatibilities:
- CRITICAL:
formData()returns wrong type: Returns plain object instead of FormData instance- Spec requires:
Promise<FormData> - Implementation returns:
Promise<Object>(plain object with key-value pairs) - Impact:
instanceof FormDatachecks fail, FormData methods unavailable
- Spec requires:
- Missing
bytes()method: Spec requiresPromise<Uint8Array> - No bodyUsed lock enforcement: Calling body methods twice doesn't throw TypeError
- No Content-Type inference: Spec auto-sets Content-Type based on body type (Blob, FormData, etc.)
Reinventing the wheel:
- The plain object return from
formData()is a significant deviation that breaks FormData-based workflows - No FormData class exists in the codebase at all
Request (lib/lws/request.js)
Conformance: Medium (60%)
Implemented:
- Core properties:
url,method,headers,credentials,mode,signal clone()method- Body mixin integration
Incompatibilities:
- Missing properties:
destination,referrerPolicy,cache,integrity,redirect,keepalive,duplex,priority clone()doesn't check bodyUsed: Spec requires TypeError if body already consumed- Non-standard: Cache-busting
_=query parameter for no-store/no-cache
Response (lib/lws/response.js)
Conformance: Medium (70%)
Implemented:
- Static methods:
Response.error(),Response.redirect(),Response.json() clone()methodokgetter- Body mixin integration
Incompatibilities:
- CRITICAL:
statusis a method, not a property- Spec requires:
readonly attribute unsigned short status - Implementation:
status(code)is a chainable setter method,statusCodeis the getter - Impact:
response.status === 200compares function to number (always false) - This is the most serious spec violation in the Fetch implementation
- Spec requires:
- Missing
bytes()method: Spec requiresPromise<Uint8Array> clone()doesn't check bodyUsed: Spec requires TypeError if body already consumed- Non-standard properties:
statusCodegetter (workaround for status-as-method)
Reinventing the wheel:
- The
statusas method design is a fundamental architectural mistake that breaks all standard Response handling code - Should have been implemented as a property from the start
fetch() (lib/fetch.js)
Conformance: Medium (65%)
Implemented:
- Basic HTTP/HTTPS requests
- Headers, body, method support
- AbortSignal (partial)
- TLS configuration
- HTTP/2 support
Incompatibilities:
- No redirect following: Spec requires automatic redirect handling with
redirectoption - No CORS support: Spec requires
modeoption enforcement (same-origin, cors, no-cors) - No Request input: Spec allows
fetch(request)but implementation only accepts URL strings - Wrong error type: Throws
ConnectionErrorinstead of spec-mandatedTypeErrorfor network errors - Incomplete AbortSignal:
- Doesn't throw
AbortError(DOMException) on abort - Overwrites existing
signal.onaborthandler - No pre-check if signal already aborted
- Doesn't throw
- Missing options:
cache,integrity,referrer,referrerPolicy,keepalive,duplex,priority
Reinventing the wheel:
- The custom
ConnectionErrorclass instead of standardTypeErrorbreaks error handling patterns - Should use standard web error types
WHATWG URL API
Reference: https://url.spec.whatwg.org/
URL (lib/lws/url.js)
Conformance: Very High (95%)
Implemented:
- All properties:
href,origin,protocol,username,password,host,hostname,port,pathname,search,searchParams,hash - All methods:
toString(),toJSON() - Static methods:
URL.canParse(),URL.parse() - Full URL parser state machine (not regex-based)
Known Limitations (documented):
- No IDNA/Punycode for non-ASCII domains (ASCII domains unaffected)
- Validation errors silently ignored (spec allows this)
Not Implemented:
URL.createObjectURL()/URL.revokeObjectURL()- browser-only, not part of core URL spec
URLSearchParams (lib/lws/url.js)
Conformance: Complete (100%)
Implemented:
- All constructor forms: string, record, iterable, URLSearchParams, empty
- All methods:
append(),delete(),get(),getAll(),has(),set(),sort(),forEach(),keys(),values(),entries(),toString() - All properties:
sizegetter - Iteration:
[Symbol.iterator]()
No incompatibilities detected.
WHATWG Streams API
Reference: https://streams.spec.whatwg.org/
Implementation: lib/lws/streams.js (ported from web-streams-polyfill)
Conformance: Complete (100%)
Implemented:
- ReadableStream: Full implementation including byte streams and BYOB reader
- WritableStream: Full implementation with default writer
- TransformStream: Full implementation
- Queuing strategies:
ByteLengthQueuingStrategy,CountQueuingStrategy - All methods:
pipeTo(),pipeThrough(),tee(),cancel(),getReader(),getWriter() - All reader/writer types: DefaultReader, BYOBReader, DefaultWriter
- All controller types: DefaultController, ByteStreamController, BYOBRequest
No incompatibilities detected.
WHATWG DOM API
Reference: https://dom.spec.whatwg.org/
AbortController / AbortSignal (lib/lws/abort.js)
Conformance: High (90%)
Implemented:
- AbortController:
signalproperty,abort(reason)method - AbortSignal:
aborted,reason,onabort,throwIfAborted() - Static methods:
AbortSignal.timeout(),AbortSignal.any(),AbortSignal.abort() - Extends EventTarget
Incompatibilities:
- DOMException fallback: Uses plain
Errorwithname: 'TimeoutError'instead ofDOMExceptionwhendocumentis undefined (QuickJS runtime limitation) - Non-standard: Adds
reasonto event object (spec only puts it on signal)
EventTarget (lib/lws/events.js)
Conformance: Minimal (40%)
Implemented:
- Core triad:
addEventListener(),removeEventListener(),dispatchEvent() on${type}handler dispatch- Listener ordering (insertion order)
Incompatibilities:
- No options parameter:
addEventListenerdoesn't supportcapture,once,passive,signaloptions - No Event class: Events are plain objects, not Event instances
- No propagation model: No bubbling/capturing, no
stopPropagation(),stopImmediatePropagation() - No cancelable events: No
preventDefault(),defaultPrevented - Missing event properties:
eventPhase,currentTarget,isTrusted,timeStamp,bubbles,cancelable,composed - No duplicate prevention: Same listener can be added multiple times
Reinventing the wheel:
- The minimal EventTarget is sufficient for internal use (AbortSignal, WebSocket) but not for general DOM-like event handling
- For a server-side runtime, the full DOM event model is likely overkill, but the options parameter (especially
onceandsignal) would be useful
WHATWG WebSocket API
Reference: https://html.spec.whatwg.org/multipage/web-sockets.html
WebSocket (lib/websocket.js)
Conformance: High (85%)
Implemented:
- Constructor:
new WebSocket(url, protocols) - Core methods:
send(),close() - Properties:
readyState,binaryType,protocol,extensions - Event handlers:
onopen,onmessage,onclose,onerror - Constants:
CONNECTING,OPEN,CLOSING,CLOSED
Incompatibilities:
- Missing
urlproperty: Constructor receives URL but doesn't store it - Missing
bufferedAmount: lws doesn't expose write-queue byte counts to JS - Constructor options incomplete: Doesn't extract
signal(AbortSignal) orheadersfrom options object
Reinventing the wheel:
- The missing
urlproperty is a simple oversight that should be fixed
WebSocketStream (lib/websocketstream.js)
Conformance: High (90%)
Reference: https://github.com/whatwg/websockets/blob/main/WebSocketStream.md (draft spec)
Implemented:
- Constructor:
new WebSocketStream(url, options) - Promises:
opened,closed - Properties:
url - Methods:
close({ closeCode, reason }) - Options:
signal,protocols
Incompatibilities:
- No direct
closeCode/closeReasonproperties: Only available viaclosedpromise (consistent with draft spec)
No significant incompatibilities. The implementation closely follows the draft spec.
Bun.js Server API
Reference: https://bun.sh/docs/api/http
Server Object (lib/serve.js)
Conformance: Low (35%)
Implemented:
server.port- actual bound port (usesvhost.listenPort)server.hostnameserver.stop(closeActiveConnections?)- partial (ignores argument, no async wait)server.upgrade(request, options?)- completeserver.publish(topic, message)- complete
Missing:
server.url- URL object (e.g.,http://localhost:3000)server.reload(options)- hot-reload handlers without restartserver.ref()/server.unref()- process lifecycle controlserver.subscriberCount(topic)- topic subscriber countserver.requestIP(request)- client IP extractionserver.timeout(request, seconds)- per-request idle timeoutserver.pendingRequests- in-flight request counterserver.pendingWebSockets- active WebSocket counterserver.closeIdleConnections()- close idle connectionsserver.development- development mode flagserver.id- server instance identifierserver.fetch(request)- internal request to running server
Impact: Many Bun.js server management patterns won't work. Users can't implement graceful shutdown, health checks, or request metrics.
WebSocketHandler Options
Conformance: Low (25%)
Implemented:
open(ws),message(ws, data),close(ws, code, reason)
Missing:
drain(ws)- backpressure relief callbackping(ws, data)- ping frame handlerpong(ws, data)- pong frame handlerperMessageDeflate- compression configurationmaxPayloadLength- message size limit (Bun default 16MB)idleTimeout- WebSocket idle timeout (Bun default 120s)backpressureLimit- backpressure threshold (Bun default 16MB)closeOnBackpressureLimit- auto-close on backpressuresendPings- automatic ping frames (Bun default true)publishToSelf- include sender inws.publish()
Impact: Can't implement backpressure handling, compression, or WebSocket lifecycle management.
ServerWebSocket
Conformance: High (75%)
Implemented:
ws.data- per-socket contextws.readyState- connection statews.send(data),ws.close(code?, reason?)- Pub/sub:
ws.subscribe(),ws.unsubscribe(),ws.publish(),ws.isSubscribed()
Missing:
ws.remoteAddress- client address getterws.subscriptions- array of subscribed topicsws.cork(callback)- batch writes into one syscall
Impact: Can't access client IP from WebSocket handlers or inspect subscription state.
serve() Options
Conformance: Medium (60%)
Implemented:
port,hostname,fetch,websocket,tls,routes
Missing:
error(err)- uncaught error handlerdevelopment- development mode flagunix- UNIX domain socket supportmaxRequestBodySize- request size limitidleTimeout- connection idle timeout (Bun default 10s)http3- HTTP/3 support (experimental, not applicable to lws)
Bun.js TCP API
Reference: https://bun.sh/docs/api/tcp
TCPSocket (lib/tcpsocket.js)
Conformance: Medium (65%)
Implemented:
socket.write(data)/socket.send(data)socket.end()/socket.close()socket.data- per-socket contextsocket.remoteAddress,socket.localPort,socket.localAddress,socket.remotePortsocket.readyState
Missing:
socket.destroy()- immediate forced close (vs gracefulend())socket.ref()/socket.unref()- process lifecycle controlsocket.cork(callback)- batch writessocket.timeout(ms)- per-socket idle timeoutsocket.connectTimeout- connection timeoutsocket.reload(handlers)- hot-reload handlerssocket.flush()- flush write buffer
Handler Callbacks:
- Implemented:
open,data,close,error,connectError(client only) - Missing:
drain,end(client only),timeout(client only)
TCP Listener
Conformance: Low (40%)
Implemented:
listener.stop(force?)- partial (ignores argument)listener.ref()/listener.unref()- present but no-op
Missing:
listener.port- bound portlistener.hostname- bound hostnamelistener.unix- UNIX socket pathlistener.reload(options)- hot-reload handlerslistener.data- arbitrary listener data
Impact: Can't inspect listener configuration or hot-reload handlers.
Bun.js UDP API
Reference: https://bun.sh/docs/api/udp
UDPSocket (lib/udpsocket.js)
Conformance: Low (20%)
Implemented:
socket.close()socket.remoteAddress,socket.remotePort,socket.localAddress,socket.localPort
Incompatibilities:
send()signature differs:- Bun:
socket.send(data, port, address) - Implementation:
socket.sendTo(data, peer)wherepeeris a sockaddr object
- Bun:
- Different creation API:
- Bun:
await Bun.udpSocket(options)withconnectoption - Implementation:
new UDPSocket(options)with constructor args
- Bun:
Missing:
socket.sendMany(packets)- batch sendsocket.setBroadcast(enable)- broadcast modesocket.setTTL(ttl)- IP TTLsocket.addMembership(address, interface?)- multicast joinsocket.dropMembership(address)- multicast leavesocket.setMulticastTTL(ttl)- multicast TTLsocket.setMulticastLoopback(enable)- multicast loopbacksocket.setMulticastInterface(address)- multicast interfacesocket.addSourceSpecificMembership(source, group)- SSM joinsocket.dropSourceSpecificMembership(source, group)- SSM leave
Handler Callbacks:
- Implemented:
open,close,error - Partial:
data(different signature:{ data, size, peer }vs Bun's(socket, buf, port, addr)) - Missing:
drain
Impact: Can't implement multicast, broadcast, or socket options. The send() signature incompatibility breaks Bun.js UDP code.
W3C File API
Reference: https://www.w3.org/TR/FileAPI/
File (lib/lws/multipart.js)
Conformance: Medium (60%)
Implemented:
file.name- filenamefile.type- MIME typefile.lastModified- modification timestampfile.stream()- ReadableStream (one-shot, not re-readable)file.arrayBuffer()- read entire filefile.text()- read as text
Missing:
file.size- file size in bytes (deliberately omitted: stream-backed design can't know size without buffering)file.slice(start?, end?, type?)- create Blob subset (deliberately omitted: stream-backed design can't re-read)file.bytes()- read as Uint8Array (newer File API addition)
Design Decision: The implementation uses a streaming design that reads file data on-demand rather than buffering the entire file. This is more memory-efficient for large uploads but means size and slice() can't be implemented without buffering.
Impact: Code that checks file size before processing or uses slice() for chunked uploads won't work.
MultipartFormData
Note: This is a sending-side helper for encoding multipart bodies, not a W3C FormData implementation. It lacks all FormData methods (get(), set(), append(), etc.).
Express-style Router (Non-standard)
App/Router (lib/lws/app.js)
Conformance: Complete (100%)
Implemented:
- All HTTP methods:
use(),get(),post(),put(),delete(),patch(),head(),options(),all() - Path matching:
:nameparameters,*wildcards - Sub-router mounting:
app.use('/api', router) - Error handling middleware: 4-arg
(err, req, res, next)signature req.params,req.path,req.appnext(err)error propagation
No incompatibilities detected. This is not a Bun.js API but follows Express conventions correctly.
Middleware (lib/lws/middleware.js)
Conformance: Complete (100%)
Implemented:
json(opts?)- JSON body parserurlencoded(opts?)- form body parserraw(opts?)- raw body parsertext(opts?)- text body parsercookies()- cookie parser (no-op, cookies already on request)cors(opts?)- CORS headerslogger(format?)- request loggingsecure(opts?)- security headers
All middleware follows Express conventions with proper async support and error handling.
Recommendations
Critical Fixes (High Priority)
Response.status: Convert from method to property
- Current:
response.status(code)/response.statusCode - Required:
response.status(readonly property) - Impact: All standard Response handling code currently broken
- Status: ✓ DONE (August 13, 2026)
- Current:
Body.formData(): Return FormData instance instead of plain object
- Requires implementing a FormData class
- Current code that uses
formData()will need updates
fetch() Request input: Accept Request objects as first argument
- Current:
fetch(url, options)only - Required:
fetch(request)orfetch(url, options)
- Current:
Headers iteration order: Sort headers lexicographically
- Current: insertion order
- Required: sorted order per spec
Response vs ServerResponse Untangling (COMPLETED)
The Response class (WHATWG Fetch API) and ServerResponse class (Express-style middleware) have been properly separated to preserve both API patterns.
Status: ✅ COMPLETE (commit e139858, August 13, 2026)
The Solution:
Responsefollows WHATWG: readonlystatusproperty, constructor-based, immutable after constructionServerResponsefollows Express: chainablestatus(code)method, mutable until sent, streaming-oriented- Client-side Response objects created with status/headers when established (not mutated after)
serve.jsbridges them viaflush()function that copies Response properties onto ServerResponse
Implementation:
lib/lws/response.js: Response has readonly getters for status/statusText/headers/url; ServerResponse has chainable methodslib/lws/protocols.js: Client Response created inonEstablishedClientHttp()with all properties (status, headers, redirected)lib/lws/app.jsandlib/lws/middleware.js: Useres.status(code)chaining (ServerResponse pattern)lib/serve.js:flush()copies Response properties onto ServerResponse for the bridge pattern
Key Design Decisions:
- Both classes extend Body (code reuse for headers, body handling)
- Response follows WHATWG spec (immutable, declarative) for fetch() clients and standard web code
- ServerResponse follows Express conventions (mutable, imperative) for middleware compatibility
- Inheritance preserves code reuse while keeping APIs separate
- No breaking changes: existing middleware continues to work with ServerResponse chaining
Impact:
- ✅ Preserves WHATWG compatibility for fetch() clients and standard web code
- ✅ Preserves Express compatibility for middleware (cors, helmet, etc.)
- ✅ Both APIs remain available for their intended use cases
- ✅ All tests pass (middleware, app, fetch, response unit tests)
See CLAUDE.md for full architectural assessment and historical context.
High Priority Improvements
- Add
bytes()method to Body mixin - Fix fetch() error types: Use TypeError instead of ConnectionError for network errors
- Complete AbortSignal handling in fetch(): throw AbortError, check pre-aborted state
- Add WebSocket.url property
- Implement EventTarget options: at least
onceandsignalparameters - Add Server.url property for Bun.js compatibility
Medium Priority
- Implement missing Server methods:
subscriberCount(),requestIP(),timeout() - Add WebSocket handler options:
drain,perMessageDeflate,maxPayloadLength,idleTimeout - Add ServerWebSocket properties:
remoteAddress,subscriptions - Implement TCPSocket methods:
destroy(),timeout(), handler callbacksdrain/end/timeout - Fix UDPSocket.send() signature to match Bun.js:
send(data, port, address)
Low Priority
- Add UDP multicast methods:
setBroadcast(),setTTL(),addMembership(), etc. - Implement Server lifecycle methods:
reload(),ref()/unref(),closeIdleConnections() - Add File.size property (requires buffering or lws API support)
- Implement Request missing properties:
destination,referrerPolicy,cache,integrity, etc.
Conclusion
The qjs-lws implementation provides a solid foundation with excellent conformance to WHATWG standards in core areas (Streams, URL, WebSocket). However, the Fetch API implementation has several critical incompatibilities that will break standard-compliant code, most notably the Response.status method-vs-property issue and the formData() return type.
The Bun.js API compatibility is partial, with significant gaps in server management, WebSocket configuration, and UDP functionality. While the core serve() API works, many advanced features and management patterns aren't supported.
The implementation does include some "reinventing the wheel" patterns (custom error types, non-standard extensions) that should be reconsidered in favor of standard web APIs where possible.
Priority Focus Areas:
- Fix Response.status (critical spec violation)
- Implement FormData class and fix formData() return type
- Add Request input support to fetch()
- Fix Headers iteration order
- Complete Bun.js Server API surface
With these fixes, qjs-lws would achieve much higher compatibility with both web standards and Bun.js applications.