📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-07-01 16:02:41 +00:00
parent 8301f01888
commit c824ba9d7b
2449 changed files with 555104 additions and 9259 deletions
@@ -0,0 +1,145 @@
# Hono WebSocket helper on Neon Functions
Neon Functions accept WebSockets via a `{ fetch, upgrade }` default export, where `upgrade(req, socket, head)` is the raw Node handshake (see the [WebSocket servers](../SKILL.md#websocket-servers) section). That works directly with the `ws` library. If you'd rather declare WebSocket routes _inside_ a Hono app — `app.get("/ws", upgradeWebSocket(...))` with the standard `onOpen`/`onMessage`/`onClose` lifecycle — you need an adapter.
## Why an adapter (and why not `@hono/node-ws`)
Hono's `upgradeWebSocket()` is runtime-agnostic at the route layer, but the actual handshake is done by a per-runtime adapter (`hono/cloudflare-workers`, `hono/deno`, `hono/bun`, `@hono/node-server`). **There is no adapter for Neon.** The Node one, `@hono/node-ws`, is **deprecated** and its replacement assumes it owns the HTTP server (`serve({ websocket })`) — which Neon's runtime does instead.
So vendor the small adapter below. It depends only on `hono` and `ws` (no deprecated package), and is adapted from `@hono/node-ws` (MIT). Instead of attaching to an `http.Server`'s `'upgrade'` event, it returns a ready-to-export `{ fetch, upgrade }` handler that matches Neon's contract.
## The adapter
```typescript
// src/hono-ws.ts — bridges Hono's upgradeWebSocket() to Neon's { fetch, upgrade }.
import { STATUS_CODES, type IncomingMessage } from "node:http";
import type { Duplex } from "node:stream";
import type { Hono } from "hono";
import { WSContext, defineWebSocketHelper } from "hono/ws";
import { WebSocketServer, type WebSocket } from "ws";
type Wire = (ws: WebSocket) => void;
export function createNeonWebSocket(app: Hono, baseUrl = "http://localhost") {
const wss = new WebSocketServer({ noServer: true });
// Correlate a handshake to its route handler via the per-request env object identity.
const pending = new Map<unknown, Wire>();
const upgradeWebSocket = defineWebSocketHelper(async (c, events, options) => {
if (c.req.header("upgrade")?.toLowerCase() !== "websocket") return;
const url = c.req.url;
pending.set(c.env, (ws) => {
const onError = options?.onError ?? ((e: unknown) => console.error(e));
const ctx = new WSContext<WebSocket>({
send: (data, opts) => ws.send(data, { compress: opts?.compress }),
close: (code, reason) => ws.close(code, reason),
raw: ws,
url,
protocol: ws.protocol,
get readyState() {
return ws.readyState;
},
});
try {
events.onOpen?.(new Event("open"), ctx);
} catch (e) {
onError(e);
}
ws.on("message", (data, isBinary) => {
for (const chunk of Array.isArray(data) ? data : [data]) {
try {
const payload = isBinary
? chunk instanceof ArrayBuffer
? chunk
: chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength)
: chunk.toString("utf-8");
events.onMessage?.(new MessageEvent("message", { data: payload }), ctx);
} catch (e) {
onError(e);
}
}
});
ws.on("close", (code, reason) => {
try {
events.onClose?.(new CloseEvent("close", { code, reason: reason.toString() }), ctx);
} catch (e) {
onError(e);
}
});
// Node 24 has no global ErrorEvent; a browser's ws.onerror gets a plain Event anyway.
ws.on("error", (error) => {
onError(error);
try {
events.onError?.(new Event("error"), ctx);
} catch (e) {
onError(e);
}
});
});
return new Response();
});
const handler = {
fetch: (request: Request) => app.fetch(request),
async upgrade(req: IncomingMessage, socket: Duplex, head: Buffer) {
const url = new URL(req.url ?? "/", baseUrl);
const headers = new Headers();
for (const [key, value] of Object.entries(req.headers)) {
if (value) headers.append(key, Array.isArray(value) ? value[0] : value);
}
// The env object identity links this request back to the route handler above.
const env = { incoming: req, outgoing: undefined };
const response = await app.request(url, { headers }, env);
const wire = pending.get(env);
pending.delete(env);
if (!wire) {
socket.end(`HTTP/1.1 ${response.status} ${STATUS_CODES[response.status] ?? ""}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
return;
}
wss.handleUpgrade(req, socket, head, (ws) => wire(ws));
},
};
return { upgradeWebSocket, handler };
}
```
How it works: the `upgrade` handler runs the handshake request through `app.request(...)` so Hono's router matches the `upgradeWebSocket` route; the helper registers a `wire` callback keyed by the per-request `env` object; if a route matched, `wss.handleUpgrade` accepts the socket and `wire` attaches the lifecycle handlers, otherwise the socket is closed with the route's status (e.g. 401/404).
## Usage
Because the handshake is routed through `app.request`, **auth and other gating are just normal Hono middleware on the route** — verify the `?token=` and return 401 before the upgrade:
```typescript
// src/index.ts
import { Hono } from "hono";
import { createNeonWebSocket } from "./hono-ws";
const app = new Hono();
const { upgradeWebSocket, handler } = createNeonWebSocket(app);
app.get("/", (c) => c.text("ok"));
app.get(
"/ws",
async (c, next) => {
const identity = await verifyToken(c.req.query("token")); // your JWT check
if (!identity) return c.text("Unauthorized", 401);
await next();
},
upgradeWebSocket(() => ({
onOpen: (_evt, ws) => ws.send("welcome"),
onMessage: (evt, ws) => ws.send(`echo: ${evt.data}`),
onClose: () => console.log("disconnected"),
})),
);
export default handler; // Neon's { fetch, upgrade } contract
```
## Caveats
- **No header-modifying middleware on the WS route.** Per [Hono's docs](https://hono.dev/docs/helpers/websocket), middleware that changes headers (e.g. CORS) on an `upgradeWebSocket` route throws ("can't modify immutable headers"), because the helper rewrites headers internally. Auth middleware that only reads the query/headers and returns 401 is fine.
- **Send a heartbeat.** A socket is dropped once it goes silent — Neon's window is 15 minutes ([Timeouts](../SKILL.md#timeouts-and-runtime-limits)), but intermediary proxies are usually far stricter (tens of seconds). Send a keepalive every ~2530s so the connection never goes quiet. With the raw `ws` socket use `ws.ping()` (the browser auto-replies with a pong); through this helper you don't hold the raw socket, so send an app-level `ws.send("ping")` the client ignores instead. See [Heartbeat](../SKILL.md#heartbeat-keep-the-socket-alive).
- **Fan-out and reconnect still apply.** This adapter only covers the per-isolate handshake. For a genuinely shared chat across isolates, broadcast with Postgres `LISTEN`/`NOTIFY`, and have clients reconnect with backoff — see [Fan-out across isolates](../SKILL.md#fan-out-across-isolates-do-not-skip-this) and [Client must reconnect](../SKILL.md#client-must-reconnect).
- **Node-only globals.** Relies on `Event`, `MessageEvent`, and `CloseEvent` (all present on Neon's Node 24 runtime). `ErrorEvent` is intentionally avoided since it isn't a Node global.