npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

http-air

v1.3.9

Published

HTTP library for batched RPC calls and real-time server-push over a single endpoint.

Readme

http-air

HTTP library for batched RPC calls and real-time server-push over a single endpoint.

  • RPC — calls made in the same tick are grouped into one HTTP request. The server executes them concurrently and streams each result back as it resolves, matched to the original call by index. The client resolves each promise individually as its result arrives.
  • Events — server pushes named events to subscribed clients over a persistent HTTP connection, with heartbeat and auto-reconnect.

Why http-air

Single endpoint, two transports. RPC and event streaming share one URL and one framework route. No WebSocket upgrade negotiation, no separate SSE endpoint to manage.

Automatic batching. RPC calls queued in the same tick are merged into one HTTP request with no configuration. Ten calls become one round trip. Duplicate calls (same method and params) are deduplicated and resolved from the same result.

Streaming results. The server responds to each batch as soon as individual calls resolve — fast methods return immediately without waiting for slow ones. The client matches results to promises by index.

Plain HTTP. Works through any proxy, CDN, or load balancer that handles standard HTTP POST requests. No protocol upgrades, no persistent TCP connections for RPC.

Auto-reconnect built in. The event connection reconnects automatically on drop, resubscribes to all active events, and detects dead connections via heartbeat — without any user code.

Framework agnostic. Ships with adapters for Express, Next.js, Hono, and Micro. Any other framework is supported by implementing a two-method interface.

Zero config to start. Both Client and Server work out of the box. URL, fetch, deduplication, and reconnection all have sensible defaults.

Quick start

Server

import express from 'express'
import { Server } from 'http-air/server'
import { expressHandler } from 'http-air/server/adapters/express'

const server = new Server()

server.handleRpc(async ({ method, params }) => {
  return myService[method](...params)
})

// push an event whenever something happens
setInterval(() => {
  server.notifyEvent('price-update', { symbol: 'BTC', price: 70000 })
}, 5000)

const app = express()
app.post('/api', expressHandler(server))

Client

import { Client } from 'http-air/client'

const client = new Client({ url: '/api' })

// RPC
const result = await client.callRpc('double', [10])

// Events
const unsubscribeEvent = client.subscribeEvent('price-update', (event) => {
  console.log(event.data)
})

// later
unsubscribeEvent()
client.disconnect()

Server

Server composes the router, RPC handler, and event emitter into a single instance.

import { Server } from 'http-air/server'

const server = new Server()

server.handleRpc(async ({ method, params }) => {
  return myService[method](...params)
})

// validate session before accepting an events connection
server.handleEventsConnect(async (req, res) => {
  const token = req.getHeader('authorization')
  if (!isValid(token)) throw new Error('unauthorized')
})

// push to all subscribers whenever something happens
server.notifyEvent('price-update', { symbol: 'BTC', price: 70000 })

Framework adapters

Each adapter takes a Server instance and returns a framework-specific handler. Body parsing is handled inside the adapter.

Express

import express from 'express'
import { expressHandler } from 'http-air/server/adapters/express'

const app = express()
app.post('/api', expressHandler(server))

Next.js

import { nextHandler } from 'http-air/server/adapters/next'

export default nextHandler(server)

Hono

import { Hono } from 'hono'
import { honoHandler } from 'http-air/server/adapters/hono'

const app = new Hono()
app.post('/api', honoHandler(server))

Micro

import { microHandler } from 'http-air/server/adapters/micro'

export default microHandler(server)

Custom adapter

Implement ServerReq and ServerRes to support any other framework:

import { ServerReq, ServerRes } from 'http-air/server'

server.handleHttp(
  {
    getHeader: (key) => req.headers[key],
    getUrl: () => req.url,
    getMethod: () => req.method,
    getBody: () => req.body,
  },
  {
    write: (content) => res.write(content),
    isClosed: () => res.writableEnded,
    writeHead: (status, headers) => res.writeHead(status, headers),
    flushHeaders: () => res.flushHeaders(),
    onClose: (cb) => res.on('close', cb),
    end: () => res.end(),
    destroy: () => res.destroy(),
  }
)

Lower-level API

For custom setups, use the building blocks directly:

import { Router, RpcServer, EventsServer } from 'http-air/server'

const router = new Router()
const rpc = new RpcServer(router)
rpc.handle(handler)
const events = new EventsServer(router)
events.handleEventsConnect(async (req, res) => {
  if (!isValid(req.getHeader('authorization'))) throw new Error('unauthorized')
})

Client

Client composes RpcClient and EventsClient behind a single instance. Use it when you need both RPC and events from the same endpoint.

import { Client } from 'http-air/client'

const client = new Client({ url: '/api' })

All clients accept the same base config:

interface ClientConfig {
  url?: string                     // endpoint URL, defaults to current page location
  fetch?: typeof globalThis.fetch  // custom fetch implementation
  init?: RequestInit               // base RequestInit merged into every request
}

RpcClient and Client also accept:

interface RpcClientConfig extends ClientConfig {
  batch?: boolean                                // group calls in the same tick, default true
  deduplicate?: boolean                          // deduplicate calls by method+params, default true
  onResponse?: (resp: ResponseMessage) => void  // called on each result or error message
}

RPC

Calls made in the same tick are grouped into one HTTP request automatically. Duplicate calls (same method + params) within a batch are deduplicated.

import { RpcClient } from 'http-air/client'

const client = new RpcClient({ url: '/api' })

// these three calls go out in a single request on the next tick
const [r1, r2, r3] = await Promise.all([
  client.call('double', [10]),
  client.call('add', [1, 2]),
  client.call('double', [10]), // deduplicated — resolved from the same result as r1
])

Events

Connects automatically on the first subscribe() call and disconnects when all subscriptions are removed. Multiple subscribe/unsubscribe calls in the same tick are batched. Auto-reconnect is always enabled.

import { EventsClient } from 'http-air/client'

const events = new EventsClient({ url: '/api' })

const unsubscribe = events.subscribe('price-update', (event) => {
  console.log(event.data)
})

// later — disconnects automatically when no subscriptions remain
unsubscribe()

Protocol

All requests use POST with the action query param. Unknown actions return 400.

RPC (action=rpc)

Requests are sent as \n\n-delimited JSON. The server responds with 207 and streams results back as they complete — out of order is fine, matched by index.

→ POST /api?action=rpc
{"index":0,"method":"double","params":[5]}\n\n
{"index":1,"method":"increment","params":[9]}\n\n

← 207
{"index":1,"result":10}\n\n
{"index":0,"result":10}\n\n

Errors are returned inline:

{"index":0,"error":{"name":"Error","message":"something went wrong"}}\n\n

Events (action=events-connect)

The client opens a persistent connection. The server streams \n\n-delimited JSON indefinitely. A heartbeat fires every 25 seconds to detect dead connections; the client reconnects automatically if one is missed.

→ POST /api?action=events-connect
← 200 (connection held open)
{"type":"heartbeat","ts":1234567890}\n\n
{"type":"event","name":"price-update","data":{...}}\n\n

Subscribe and unsubscribe send event names as \n\n-delimited objects in the body:

→ POST /api?action=events-subscribe
{"name":"price-update"}\n\n
{"name":"order-filled"}\n\n

→ POST /api?action=events-unsubscribe
{"name":"order-filled"}\n\n

License

MIT © Yosbel Marín