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

@livequery/honojs

v2.0.155

Published

Hono adapter for @livequery ecosystem

Readme

@livequery/honojs

Hono framework adapter for the @livequery ecosystem. Provides request parsing middleware, route registration helpers, response utilities, an API gateway, and a service linker for building livequery-compatible REST APIs with Hono.


Installation

npm install @livequery/honojs @livequery/core hono
# ws is required for WebSocket gateway support
npm install ws

Peer dependencies: hono >= 4.12, ws >= 8


Quick start: simple service

Use createLivequery to register routes. It automatically applies the livequery middleware to every route and tracks paths in a registry for service discovery.

import * as http from 'http'
import { Hono } from 'hono'
import {
    createLivequery,
    getLivequeryRequest,
    livequeryJson,
    HonoApiServiceLinker,
    WebsocketGateway,
} from '@livequery/honojs'

const app = new Hono()
const server = http.createServer((req, res) => {
    app.fetch(new Request(`http://localhost${req.url}`, {
        method: req.method,
        headers: req.headers as HeadersInit,
    })).then(async response => {
        res.writeHead(response.status, Object.fromEntries(response.headers.entries()))
        res.end(Buffer.from(await response.arrayBuffer()))
    })
})
server.listen(3001)

const websocketGateway = new WebsocketGateway(server)
const livequery = createLivequery(app, { websocketGateway })

// Collection route
livequery.get('/livequery/products', c => {
    return livequeryJson(c, {
        items: [
            { _id: 'p-1', name: 'Keyboard' },
            { _id: 'p-2', name: 'Mouse' },
        ],
    })
})

// Document route
livequery.get('/livequery/products/:id', c => {
    const req = getLivequeryRequest(c)
    return livequeryJson(c, {
        item: { id: req.document_id, name: `Product ${req.document_id}` },
    })
})

// POST / PATCH / DELETE
livequery.post('/livequery/products', c => {
    const req = getLivequeryRequest(c)
    return livequeryJson(c, { item: { id: 'new-id', ...req.body as object } }, 201)
})

// Broadcast this service's routes so a gateway can discover it
const linker = new HonoApiServiceLinker({ routes: livequery.registry, websocketGateway })
linker.start('products-service', 3001)

_id is renamed to id and other _-prefixed fields are stripped automatically by livequeryJson (see Private field stripping).


Quick start: plain Hono app

If you prefer to register routes directly on a Hono app, use collectServicePaths to extract the path list and apply the livequery() middleware manually.

import { Hono } from 'hono'
import {
    livequery,
    collectServicePaths,
    getLivequeryRequest,
    livequeryJson,
    HonoApiServiceLinker,
} from '@livequery/honojs'

const app = new Hono()

app.get('/livequery/products',
    livequery({ routePath: '/livequery/products' }),
    c => livequeryJson(c, { items: [] })
)

app.get('/livequery/products/:id',
    livequery({ routePath: '/livequery/products/:id' }),
    c => {
        const req = getLivequeryRequest(c)
        return livequeryJson(c, { item: { id: req.document_id } })
    }
)

const linker = new HonoApiServiceLinker({ routes: collectServicePaths(app) })
linker.start('products-service', 3001)

Passing routePath to livequery() is required for correct ref, collection_ref, and document_id parsing. When omitted, the actual URL path is used as ref and document_id is never populated.


API gateway

An API gateway auto-discovers upstream services over UDP and proxies HTTP requests. Client id headers (x-lcid, x-lgid) are forwarded so upstream services can register realtime subscriptions.

import * as http from 'http'
import { Hono } from 'hono'
import { HonoApiGatewayLinker, WebsocketGateway } from '@livequery/honojs'

const app = new Hono()
const server = http.createServer(/* bridge to app.fetch */)
server.listen(3000)

const websocketGateway = new WebsocketGateway(server)
const gateway = new HonoApiGatewayLinker({ websocketGateway })

app.all('*', gateway.handler())

Routes with no registered upstream return 404 API_NOT_FOUND. Unreachable upstreams return 502 SERVICE_API_OFFLINE and are marked offline; after 30 seconds with no reconnect they are removed automatically.

For manual service registration (e.g. in tests), access the underlying handler:

gateway.gateway.register({
    node_id: 'svc-1',
    hostname: '127.0.0.1',
    port: 3001,
    paths: [{ method: 'GET', path: 'livequery/products/:id' }],
})

If you need to subclass ApiGatewayHandler directly, use HonoApiGateway:

import { HonoApiGateway } from '@livequery/honojs'

class CustomGateway extends HonoApiGateway {}

const gateway = new CustomGateway({ websocketGateway })
const res = await gateway.fetch(new Request('http://gateway/livequery/products'))

Realtime subscriptions

When a client sends a GET request with the x-lcid header, the middleware registers a realtime subscription so the client receives push updates when data changes.

GET /livequery/products
x-lcid: client-abc123

The middleware calls websocketGateway.listen(...) with the resource ref and client/gateway ids. No subscription is registered when cursor params (:after, :before, :around) are present — pagination requests are one-off fetches.

// Just pass websocketGateway when creating the router — no extra code needed.
const livequery = createLivequery(app, { websocketGateway })

The x-lgid header can specify a gateway id when routing through multiple gateways. If absent, websocketGateway.id is used.


Private field stripping

livequeryJson strips private fields from item and items before sending.

Rules applied to each item:

  • _id is renamed to id (only when id is not already set).
  • Any other key starting with _ is removed.
  • All other keys pass through unchanged.
livequeryJson(c, {
    item: { _id: 'abc', name: 'Widget', _internalToken: 'xyz', price: 9.99 },
})
// Sends: { "item": { "id": "abc", "name": "Widget", "price": 9.99 } }

For array responses:

livequeryJson(c, {
    items: [
        { _id: 'p-1', name: 'Keyboard', _secret: 'hidden' },
        { _id: 'p-2', name: 'Mouse',    _secret: 'hidden' },
    ],
})
// Sends: { "items": [{ "id": "p-1", "name": "Keyboard" }, { "id": "p-2", "name": "Mouse" }] }

Objects that implement toJSON() (e.g. Mongoose documents) are serialised via toJSON() before field stripping.

To apply the transformation without sending:

import { mapLivequeryResponse } from '@livequery/honojs'

const mapped = mapLivequeryResponse({ items: [...] })

LivequeryRequest field reference

getLivequeryRequest(c) returns a LivequeryRequest<unknown> from @livequery/core.

Given route /livequery/products/:id and request GET /livequery/products/p-1?limit=20:

| Field | Example | Description | |---|---|---| | ref | "products/p-1" | Canonical resource reference (path minus prefix) | | collection_ref | "products" | Reference to the parent collection | | schema_collection_ref | "products" | Like collection_ref but :param markers replaced with key names | | document_id | "p-1" | Document id; undefined for collection routes | | keys | { id: "p-1" } | Hono route params | | method | "GET" | HTTP method, always uppercase | | body | { name: "..." } | Parsed JSON body; undefined for GET/HEAD | | query | { limit: "20" } | Query string params | | path | "/livequery/products/p-1" | Literal URL path |

For collection routes (last route segment is not a :param), document_id is undefined and ref equals collection_ref.


createDatasourceMapper

For services backed by a typed datasource adapter, createDatasourceMapper generates Hono handlers without boilerplate.

import { Subject } from 'rxjs'
import { createDatasourceMapper, createLivequery, livequeryJson } from '@livequery/honojs'
import type { LivequeryDatasource } from '@livequery/honojs'
import type { LivequeryBaseEntity, LivequeryRequest, WebsocketSyncPayload } from '@livequery/types'

type Product   = LivequeryBaseEntity & { name: string }
type Config    = { db: Product[] }
type RouteOpts = { collection: 'products' }

class ProductDatasource
    extends Subject<WebsocketSyncPayload<LivequeryBaseEntity>>
    implements LivequeryDatasource<Config, RouteOpts>
{
    #config!: Config

    async init(config: Config): Promise<void> { this.#config = config }

    async query(req: LivequeryRequest, opts: RouteOpts) {
        const items = this.#config[opts.collection]
        if (req.is_collection) return { items }
        return { item: items.find(p => p.id === req.doc_id) }
    }
}

const app = new Hono()
const livequery = createLivequery(app)

// Placeholder handlers — replaced after async init
let listHandler = (c: any) => livequeryJson(c, { items: [] })
let itemHandler = (c: any) => livequeryJson(c, { item: {} })

livequery.get('/livequery/products',     c => listHandler(c))
livequery.get('/livequery/products/:id', c => itemHandler(c))

const useDatasource = await createDatasourceMapper({
    datasource: new ProductDatasource(),
    routes: livequery.registry,
    config: { db: [{ id: 'p-1', name: 'Keyboard' }] },
})

listHandler = useDatasource({ collection: 'products' })
itemHandler = useDatasource({ collection: 'products' })

Note: the LivequeryRequest received in datasource.query() is from @livequery/types and has doc_id, is_collection, and options — distinct from the @livequery/core type returned by getLivequeryRequest(c).


TypeScript types

import type {
    // Local types
    LivequeryRoute,          // { method: string; path: string }
    LivequeryResponse,       // { item?: T; items?: T[]; [key: string]: unknown }
    LivequeryVariables,      // { livequery: LivequeryRequest<unknown> }
    LivequeryContext,        // Context<{ Variables: LivequeryVariables }>

    // Re-exported from @livequery/core
    LivequeryRequest,        // parsed request shape (core version)
    LivequeryDatasource,     // datasource contract
    LivequeryDatasourceInitConfig,
    RealtimeSubscription,
    UdpDiscoveryNode,
    UdpDiscoveryOptions,
    UdpDiscoveryPacket,
    UdpDiscoveryStatus,
} from '@livequery/honojs'