| const HOOK_NAMES = [ | ||
| "upgrade", | ||
| "message", | ||
| "open", | ||
| "close", | ||
| "drain", | ||
| "error", | ||
| "ping", | ||
| "pong" | ||
| ]; | ||
| function defaultResolve(server, wsOpts) { | ||
| if (wsOpts.resolve) return wsOpts.resolve; | ||
| if (HOOK_NAMES.some((name) => typeof wsOpts[name] === "function")) return; | ||
| const fetch = server.options.fetch; | ||
| if (typeof fetch !== "function") throw new Error("[crossws] server has no fetch handler to resolve WebSocket hooks from"); | ||
| return (req) => Promise.resolve(fetch(req)).then((res) => hooksFromFetchResult(res)); | ||
| } | ||
| function hooksFromFetchResult(res) { | ||
| const crossws = res?.crossws; | ||
| if (res instanceof Response) { | ||
| if (crossws) { | ||
| res.body?.cancel().catch(() => {}); | ||
| return crossws; | ||
| } | ||
| if (!res.ok && res.status !== 101) return { upgrade: () => res }; | ||
| res.body?.cancel().catch(() => {}); | ||
| return; | ||
| } | ||
| const headers = res?.headers; | ||
| if (!headers) return crossws; | ||
| const userUpgrade = crossws?.upgrade; | ||
| return { | ||
| ...crossws, | ||
| async upgrade(request) { | ||
| const result = await userUpgrade?.(request); | ||
| if (result instanceof Response) return result; | ||
| return { | ||
| ...result, | ||
| headers: mergeHeaders(headers, result?.headers) | ||
| }; | ||
| } | ||
| }; | ||
| } | ||
| function mergeHeaders(base, extra) { | ||
| if (!extra) return base; | ||
| const merged = new Headers(base); | ||
| for (const [key, value] of new Headers(extra)) merged.set(key, value); | ||
| return merged; | ||
| } | ||
| export { defaultResolve }; |
@@ -1,2 +0,2 @@ | ||
| import { Hooks } from "./adapter.mjs"; | ||
| import { Hooks, MaybePromise } from "./adapter.mjs"; | ||
| import { BunOptions } from "./bun.mjs"; | ||
@@ -10,2 +10,11 @@ import { BunnyOptions } from "./bunny.mjs"; | ||
| type WSOptions = Partial<Hooks> & { | ||
| /** | ||
| * Resolve the WebSocket hooks for an incoming request. | ||
| * | ||
| * When omitted, hooks are resolved by calling the server's `fetch` handler | ||
| * and reading the `crossws` property off the returned `Response`. Provide | ||
| * `resolve` only to customize routing (e.g. resolve hooks without invoking | ||
| * the app). The default is skipped when inline hooks are passed directly | ||
| * (e.g. `ws({ message })`), which run with zero per-event overhead instead. | ||
| */ | ||
| resolve?: (req: ServerRequest) => Partial<Hooks> | Promise<Partial<Hooks>>; | ||
@@ -21,5 +30,23 @@ options?: { | ||
| }; | ||
| type ServerWithWSOptions = ServerOptions & { | ||
| /** | ||
| * Value the app `fetch` handler may return for a WebSocket upgrade request when | ||
| * the default resolver is used. Either: | ||
| * - a `Response` carrying hooks on its `crossws` property (the srvx convention), | ||
| * or | ||
| * - a plain `{ crossws, headers }` object with the hooks and optional headers to | ||
| * send on the WebSocket handshake response. | ||
| * | ||
| * Returning a normal `Response` (no `crossws`) is always valid — the connection | ||
| * simply upgrades without hooks. | ||
| */ | ||
| type WSUpgradeResult = (Response & { | ||
| crossws?: Partial<Hooks>; | ||
| }) | { | ||
| crossws?: Partial<Hooks>; | ||
| headers?: HeadersInit; | ||
| }; | ||
| type ServerWithWSOptions = Omit<ServerOptions, "fetch"> & { | ||
| fetch: (request: ServerRequest) => MaybePromise<WSUpgradeResult>; | ||
| websocket?: WSOptions; | ||
| }; | ||
| export { ServerWithWSOptions, WSOptions }; |
@@ -438,2 +438,18 @@ import { WebSocket } from "./web.mjs"; | ||
| terminate(): void; | ||
| /** | ||
| * Send an application-level WebSocket ping control frame to the client. | ||
| * | ||
| * Pair with the {@link Hooks.pong} hook (e.g. embedding a timestamp in | ||
| * `data`) to measure round-trip latency, or rely on the {@link Hooks.ping} | ||
| * hook to observe pings the client sends unprompted. | ||
| * | ||
| * `data` is optional and, per RFC 6455, must not exceed 125 bytes (a ping is | ||
| * a control frame); an over-long or otherwise invalid payload is reported via | ||
| * the {@link Hooks.error} hook instead of being sent. | ||
| * | ||
| * **Note:** Not all adapters can send a ping frame; unsupported adapters | ||
| * warn once and no-op. Refer to the | ||
| * [compatibility table](https://crossws.h3.dev/guide/peer#compatibility). | ||
| */ | ||
| ping(_data?: unknown): number | void | undefined; | ||
| /** Subscribe to a topic */ | ||
@@ -519,2 +535,18 @@ subscribe(topic: string): void; | ||
| } | ||
| declare class AdapterHookable { | ||
| #private; | ||
| options: AdapterOptions; | ||
| constructor(options?: AdapterOptions); | ||
| callHook<N extends keyof Hooks>(name: N, arg1: Parameters<Hooks[N]>[0], arg2?: Parameters<Hooks[N]>[1], connection?: object): MaybePromise<ReturnType<Hooks[N]>>; | ||
| upgrade(request: Request & { | ||
| readonly context?: Record<string, unknown>; | ||
| }): Promise<{ | ||
| context: PeerContext; | ||
| namespace: string; | ||
| upgradeHeaders?: HeadersInit; | ||
| endResponse?: Response; | ||
| handled?: boolean; | ||
| }>; | ||
| _resolveProtocol(request: Request, upgradeHeaders: HeadersInit | undefined, protocolFromHook: string | undefined): Promise<string | undefined>; | ||
| } | ||
| declare function defineHooks<T extends Partial<Hooks> = Partial<Hooks>>(hooks: T): T; | ||
@@ -531,2 +563,8 @@ type ResolveHooks = (request: Request & { | ||
| * - You can return { headers } to modify the response. | ||
| * - You can return { protocol } to accept a WebSocket subprotocol for this | ||
| * connection (echoed back as `Sec-WebSocket-Protocol`). This is the | ||
| * per-connection counterpart to the global | ||
| * {@link AdapterOptions.handleProtocols} option and takes precedence over | ||
| * it. It should be one of the subprotocols the client offered (the values | ||
| * in the request's `Sec-WebSocket-Protocol` header). | ||
| * - You can return { namespace } to change the pub/sub namespace. | ||
@@ -546,2 +584,3 @@ * - You can return { context } to provide a custom peer context. | ||
| headers?: HeadersInit; | ||
| protocol?: string; | ||
| namespace?: string; | ||
@@ -570,2 +609,20 @@ context?: PeerContext; | ||
| error: (peer: Peer, error: WSError) => MaybePromise<void>; | ||
| /** | ||
| * An application-level WebSocket ping control frame was received from the | ||
| * peer (e.g. sent by the client, or by another server via | ||
| * {@link Peer.ping}). | ||
| * | ||
| * **Note:** Only emitted by adapters that surface inbound ping frames. | ||
| * Refer to the [compatibility table](https://crossws.h3.dev/guide/peer#compatibility). | ||
| */ | ||
| ping: (peer: Peer, data: Uint8Array) => MaybePromise<void>; | ||
| /** | ||
| * An application-level WebSocket pong control frame was received from the | ||
| * peer, typically in reply to {@link Peer.ping}. Use together with a | ||
| * timestamp embedded in the ping payload to measure round-trip latency. | ||
| * | ||
| * **Note:** Only emitted by adapters that surface inbound pong frames. | ||
| * Refer to the [compatibility table](https://crossws.h3.dev/guide/peer#compatibility). | ||
| */ | ||
| pong: (peer: Peer, data: Uint8Array) => MaybePromise<void>; | ||
| } | ||
@@ -608,2 +665,24 @@ interface AdapterInstance { | ||
| /** | ||
| * Select the WebSocket subprotocol to accept during the handshake. | ||
| * | ||
| * Browsers that open `new WebSocket(url, protocols)` send their offer in the | ||
| * `Sec-WebSocket-Protocol` request header and **reject the connection** if | ||
| * the server's `101` response doesn't echo one of the offered values back. | ||
| * By default crossws negotiates nothing (a server never claims to speak a | ||
| * protocol the app didn't opt into), so supply this to accept one. | ||
| * | ||
| * Called with the set of subprotocols the client offered and the upgrade | ||
| * request. Return the single subprotocol to accept (must be one of the | ||
| * offered values), or `false`/`undefined` to accept none. Only invoked when | ||
| * the client actually offered at least one subprotocol. | ||
| * | ||
| * This is the global default; the {@link Hooks.upgrade} hook may return | ||
| * `{ protocol }` to override it per connection. | ||
| * | ||
| * @example | ||
| * handleProtocols: (protocols) => | ||
| * protocols.has("graphql-transport-ws") ? "graphql-transport-ws" : false | ||
| */ | ||
| handleProtocols?: (protocols: Set<string>, request: Request) => MaybePromise<string | false | null | undefined>; | ||
| /** | ||
| * Optional sync backplane to relay pub/sub between multiple crossws | ||
@@ -623,5 +702,34 @@ * instances (e.g. across regions/processes). Opt-in: when absent, pub/sub | ||
| onError?: (error: unknown, context: SyncErrorContext) => void; | ||
| /** | ||
| * Close a connection that has stayed idle — no incoming messages and no | ||
| * pong replies — for roughly this many **seconds**. This reclaims peers | ||
| * whose transport died silently ("half-open" sockets: laptop sleep, | ||
| * NAT/mobile idle timeout, power loss, a cut cable) without the TCP stack | ||
| * ever delivering a `FIN`/`RST`, which would otherwise leak forever. | ||
| * | ||
| * Implemented per runtime, but with a single consistent knob: | ||
| * - **Node** — the `ws` library has no built-in liveness, so crossws pings | ||
| * each peer on this interval and terminates any that miss the pong. Honors | ||
| * sub-second (fractional) values. | ||
| * - **Bun / Deno / uWebSockets / Bunny** — mapped to the runtime's native | ||
| * WebSocket idle timeout, which also auto-sends keepalive pings. These | ||
| * runtimes take **whole seconds**, so a fractional value is rounded down — | ||
| * a value below `1` may become `0` and disable liveness there; use `>= 1`. | ||
| * | ||
| * Terminated peers surface through the normal `close` hook (Node reports | ||
| * code `1006`), so any `close`/`error` teardown — including | ||
| * `createWebSocketProxy` closing its upstream — runs unchanged. | ||
| * | ||
| * Set to `0` to disable. Defaults to {@link DEFAULT_IDLE_TIMEOUT} (30s) on | ||
| * every runtime — low enough to keep idle connections alive through the | ||
| * typical ~60s reverse-proxy / load-balancer idle timeout, while reclaiming | ||
| * dead sockets promptly. Pings are a few bytes and standards clients auto-pong, | ||
| * so a live connection is never disconnected. | ||
| * | ||
| * @default 30 (seconds) | ||
| */ | ||
| idleTimeout?: number; | ||
| } | ||
| type Adapter<AdapterT extends AdapterInstance = AdapterInstance, Options extends AdapterOptions = AdapterOptions> = (options?: Options) => AdapterT; | ||
| declare function defineWebSocketAdapter<AdapterT extends AdapterInstance = AdapterInstance, Options extends AdapterOptions = AdapterOptions>(factory: Adapter<AdapterT, Options>): Adapter<AdapterT, Options>; | ||
| export { Adapter, AdapterInstance, AdapterInternal, AdapterOptions, Hooks, Message, Peer, PeerContext, PostgresClientLike, RedisClientLike, ResolveHooks, SyncAdapter, SyncDriver, SyncDriverName, SyncDriverOptions, SyncErrorContext, SyncMessage, WSError, WaitForDrainOptions, broadcastChannel, cluster, decodeEnvelope, defineHooks, defineWebSocketAdapter, encodeEnvelope, pgsql, redis, setupPrimaryCluster, syncDrivers }; | ||
| export { Adapter, AdapterHookable, AdapterInstance, AdapterInternal, AdapterOptions, Hooks, MaybePromise, Message, Peer, PeerContext, PostgresClientLike, RedisClientLike, ResolveHooks, SyncAdapter, SyncDriver, SyncDriverName, SyncDriverOptions, SyncErrorContext, SyncMessage, WSError, WaitForDrainOptions, broadcastChannel, cluster, decodeEnvelope, defineHooks, defineWebSocketAdapter, encodeEnvelope, pgsql, redis, setupPrimaryCluster, syncDrivers }; |
+69
-25
| var AdapterHookable = class { | ||
| options; | ||
| #resolveCache = /* @__PURE__ */ new WeakMap(); | ||
| constructor(options) { | ||
| this.options = options || {}; | ||
| } | ||
| callHook(name, arg1, arg2) { | ||
| callHook(name, arg1, arg2, connection) { | ||
| const globalHook = this.options.hooks?.[name]; | ||
| const globalPromise = globalHook?.(arg1, arg2); | ||
| const resolve = this.options.resolve; | ||
| if (!resolve) return globalPromise; | ||
| const request = arg1.request || arg1; | ||
| const resolveHooksPromise = this.options.resolve?.(request); | ||
| const cacheKey = connection || arg1.context || request; | ||
| let resolveHooksPromise; | ||
| if (this.#resolveCache.has(cacheKey)) resolveHooksPromise = this.#resolveCache.get(cacheKey); | ||
| else { | ||
| try { | ||
| resolveHooksPromise = resolve(request); | ||
| } catch (error) { | ||
| resolveHooksPromise = Promise.reject(error); | ||
| } | ||
| this.#resolveCache.set(cacheKey, resolveHooksPromise); | ||
| if (resolveHooksPromise instanceof Promise) resolveHooksPromise.catch(() => { | ||
| if (this.#resolveCache.get(cacheKey) === resolveHooksPromise) this.#resolveCache.delete(cacheKey); | ||
| }); | ||
| } | ||
| if (!resolveHooksPromise) return globalPromise; | ||
@@ -21,25 +37,22 @@ const resolvePromise = resolveHooksPromise instanceof Promise ? resolveHooksPromise.then((hooks) => hooks?.[name]) : resolveHooksPromise?.[name]; | ||
| const context = request.context || {}; | ||
| let upgradeHeaders; | ||
| let protocolFromHook; | ||
| try { | ||
| const res = await this.callHook("upgrade", request); | ||
| if (!res) return { | ||
| context, | ||
| namespace | ||
| }; | ||
| if (res.namespace) namespace = res.namespace; | ||
| if (res.context) Object.assign(context, res.context); | ||
| if (res instanceof Response) return { | ||
| context, | ||
| namespace, | ||
| endResponse: res | ||
| }; | ||
| if (res.handled) return { | ||
| context, | ||
| namespace, | ||
| handled: true | ||
| }; | ||
| if (res.headers) return { | ||
| context, | ||
| namespace, | ||
| upgradeHeaders: res.headers | ||
| }; | ||
| const res = await this.callHook("upgrade", request, void 0, context); | ||
| if (res) { | ||
| if (res.namespace) namespace = res.namespace; | ||
| if (res.context) Object.assign(context, res.context); | ||
| if (res instanceof Response) return { | ||
| context, | ||
| namespace, | ||
| endResponse: res | ||
| }; | ||
| if (res.handled) return { | ||
| context, | ||
| namespace, | ||
| handled: true | ||
| }; | ||
| upgradeHeaders = res.headers; | ||
| protocolFromHook = res.protocol; | ||
| } | ||
| } catch (error) { | ||
@@ -54,8 +67,39 @@ const errResponse = error.response || error; | ||
| } | ||
| const protocol = await this._resolveProtocol(request, upgradeHeaders, protocolFromHook); | ||
| if (protocol) { | ||
| const merged = new Headers(upgradeHeaders); | ||
| merged.set("sec-websocket-protocol", protocol); | ||
| upgradeHeaders = merged; | ||
| } | ||
| return { | ||
| context, | ||
| namespace | ||
| namespace, | ||
| upgradeHeaders | ||
| }; | ||
| } | ||
| async _resolveProtocol(request, upgradeHeaders, protocolFromHook) { | ||
| if (protocolFromHook) return protocolFromHook; | ||
| if (upgradeHeaders) { | ||
| const fromHeader = (upgradeHeaders instanceof Headers ? upgradeHeaders : new Headers(upgradeHeaders)).get("sec-websocket-protocol"); | ||
| if (fromHeader) return fromHeader; | ||
| } | ||
| const handleProtocols = this.options.handleProtocols; | ||
| if (handleProtocols) { | ||
| const offered = _parseProtocols(request.headers.get("sec-websocket-protocol")); | ||
| if (offered.size > 0) { | ||
| const chosen = await handleProtocols(offered, request); | ||
| if (chosen) return chosen; | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| function _parseProtocols(header) { | ||
| const protocols = /* @__PURE__ */ new Set(); | ||
| if (!header) return protocols; | ||
| for (const part of header.split(",")) { | ||
| const token = part.trim(); | ||
| if (token) protocols.add(token); | ||
| } | ||
| return protocols; | ||
| } | ||
| function defineHooks(hooks) { | ||
@@ -62,0 +106,0 @@ return hooks; |
@@ -1,2 +0,2 @@ | ||
| import { Adapter, AdapterInstance, AdapterOptions, Peer, PeerContext, SyncDriver } from "./adapter.mjs"; | ||
| import { Adapter, AdapterHookable, AdapterInstance, AdapterOptions, Peer, PeerContext, SyncDriver } from "./adapter.mjs"; | ||
| import { Server, ServerWebSocket, WebSocketHandler } from "bun"; | ||
@@ -22,2 +22,3 @@ interface BunAdapter extends AdapterInstance { | ||
| sync?: SyncDriver; | ||
| hooks: AdapterHookable; | ||
| }> { | ||
@@ -37,3 +38,4 @@ get remoteAddress(): string; | ||
| terminate(): void; | ||
| ping(data?: unknown): number; | ||
| } | ||
| export { BunAdapter, BunOptions, bunAdapter }; |
@@ -14,2 +14,3 @@ import { import_websocket_server } from "./libs/ws.mjs"; | ||
| } | ||
| const HEARTBEAT_PING = Buffer.from("crossws-ping"); | ||
| const nodeAdapter = (options = {}) => { | ||
@@ -25,2 +26,5 @@ if ("Deno" in globalThis || "Bun" in globalThis) throw new Error("[crossws] Using Node.js adapter in an incompatible environment."); | ||
| }); | ||
| const liveSockets = /* @__PURE__ */ new Set(); | ||
| const idleTimeoutMs = (options.idleTimeout ?? 30) * 1e3; | ||
| const sweepMs = Math.max(1, Math.floor(idleTimeoutMs)); | ||
| wss.on("connection", (ws, nodeReq) => { | ||
@@ -35,5 +39,16 @@ const request = new NodeReqProxy(nodeReq); | ||
| namespace: nodeReq._namespace, | ||
| sync: baseUtils.sync | ||
| sync: baseUtils.sync, | ||
| hooks | ||
| }); | ||
| peers.add(peer); | ||
| liveSockets.add(ws); | ||
| if (idleTimeoutMs > 0) { | ||
| ws._isAlive = true; | ||
| const markAlive = () => { | ||
| ws._isAlive = true; | ||
| }; | ||
| ws.on("pong", markAlive); | ||
| ws.on("ping", markAlive); | ||
| ws.on("message", markAlive); | ||
| } | ||
| hooks.callHook("open", peer); | ||
@@ -45,2 +60,9 @@ ws.on("message", (data, isBinary) => { | ||
| }); | ||
| ws.on("ping", (data) => { | ||
| hooks.callHook("ping", peer, data); | ||
| }); | ||
| ws.on("pong", (data) => { | ||
| if (data.equals(HEARTBEAT_PING)) return; | ||
| hooks.callHook("pong", peer, data); | ||
| }); | ||
| ws.on("error", (error) => { | ||
@@ -55,2 +77,3 @@ peers.delete(peer); | ||
| peers.delete(peer); | ||
| liveSockets.delete(ws); | ||
| socket?.off("drain", onDrain); | ||
@@ -63,2 +86,25 @@ hooks.callHook("close", peer, { | ||
| }); | ||
| let idleTimer; | ||
| const stopSweep = () => { | ||
| if (idleTimer) { | ||
| clearInterval(idleTimer); | ||
| idleTimer = void 0; | ||
| } | ||
| }; | ||
| if (idleTimeoutMs > 0) { | ||
| idleTimer = setInterval(() => { | ||
| for (const ws of liveSockets) { | ||
| if (ws._isAlive === false) { | ||
| ws.terminate(); | ||
| continue; | ||
| } | ||
| ws._isAlive = false; | ||
| try { | ||
| ws.ping(HEARTBEAT_PING); | ||
| } catch {} | ||
| } | ||
| }, sweepMs); | ||
| idleTimer.unref?.(); | ||
| wss.on("close", stopSweep); | ||
| } | ||
| wss.on("headers", (outgoingHeaders, req) => { | ||
@@ -70,5 +116,15 @@ const upgradeHeaders = req._upgradeHeaders; | ||
| ...baseUtils, | ||
| close: async (code, reason) => { | ||
| stopSweep(); | ||
| await baseUtils.close(code, reason); | ||
| }, | ||
| handleUpgrade: async (nodeReq, socket, head, webRequest) => { | ||
| const request = webRequest || new NodeReqProxy(nodeReq); | ||
| const { upgradeHeaders, endResponse, handled, context, namespace } = await hooks.upgrade(request); | ||
| let upgraded; | ||
| try { | ||
| upgraded = await hooks.upgrade(request); | ||
| } catch { | ||
| return sendResponse(socket, new Response("Internal Server Error", { status: 500 })); | ||
| } | ||
| const { upgradeHeaders, endResponse, handled, context, namespace } = upgraded; | ||
| if (endResponse) return sendResponse(socket, endResponse); | ||
@@ -85,4 +141,4 @@ if (handled) return; | ||
| closeAll: (code, data, force) => { | ||
| for (const client of wss.clients) if (force) client.terminate(); | ||
| else client.close(code, data); | ||
| for (const ws of liveSockets) if (force) ws.terminate(); | ||
| else ws.close(code, data); | ||
| } | ||
@@ -124,2 +180,9 @@ }; | ||
| } | ||
| ping(data) { | ||
| try { | ||
| this._internal.ws.ping(data); | ||
| } catch (error) { | ||
| this._internal.hooks.callHook("error", this, new WSError(error)); | ||
| } | ||
| } | ||
| }; | ||
@@ -126,0 +189,0 @@ var NodeReqProxy = class extends StubRequest { |
@@ -94,2 +94,3 @@ import { kNodeInspect, serializeMessage } from "./adapter.mjs"; | ||
| #ws; | ||
| #pingUnsupportedWarned = false; | ||
| constructor(internal) { | ||
@@ -158,2 +159,8 @@ this._topics = /* @__PURE__ */ new Set(); | ||
| } | ||
| ping(_data) { | ||
| if (!this.#pingUnsupportedWarned) { | ||
| this.#pingUnsupportedWarned = true; | ||
| console.warn("[crossws] `peer.ping()` is not supported by this adapter."); | ||
| } | ||
| } | ||
| subscribe(topic) { | ||
@@ -160,0 +167,0 @@ this._topics.add(topic); |
| import { AdapterHookable, adapterUtils, getPeers, toBufferLike } from "../_chunks/adapter.mjs"; | ||
| import { Message, Peer } from "../_chunks/peer.mjs"; | ||
| import { WSError } from "../_chunks/error.mjs"; | ||
| const bunAdapter = (options = {}) => { | ||
@@ -24,4 +25,5 @@ if (typeof Bun === "undefined") throw new Error("[crossws] Using Bun adapter in an incompatible environment."); | ||
| websocket: { | ||
| idleTimeout: options.idleTimeout ?? 30, | ||
| message: (ws, message) => { | ||
| const peer = getPeer(ws, getPeers(globalPeers, ws.data.namespace), baseUtils.sync); | ||
| const peer = getPeer(ws, getPeers(globalPeers, ws.data.namespace), baseUtils.sync, hooks); | ||
| hooks.callHook("message", peer, new Message(message, peer)); | ||
@@ -31,3 +33,3 @@ }, | ||
| const peers = getPeers(globalPeers, ws.data.namespace); | ||
| const peer = getPeer(ws, peers, baseUtils.sync); | ||
| const peer = getPeer(ws, peers, baseUtils.sync, hooks); | ||
| peers.add(peer); | ||
@@ -38,3 +40,3 @@ hooks.callHook("open", peer); | ||
| const peers = getPeers(globalPeers, ws.data.namespace); | ||
| const peer = getPeer(ws, peers, baseUtils.sync); | ||
| const peer = getPeer(ws, peers, baseUtils.sync, hooks); | ||
| peers.delete(peer); | ||
@@ -47,4 +49,12 @@ hooks.callHook("close", peer, { | ||
| drain: (ws) => { | ||
| const peer = getPeer(ws, getPeers(globalPeers, ws.data.namespace)); | ||
| const peer = getPeer(ws, getPeers(globalPeers, ws.data.namespace), baseUtils.sync, hooks); | ||
| hooks.callHook("drain", peer); | ||
| }, | ||
| ping: (ws, data) => { | ||
| const peer = getPeer(ws, getPeers(globalPeers, ws.data.namespace), baseUtils.sync, hooks); | ||
| hooks.callHook("ping", peer, data); | ||
| }, | ||
| pong: (ws, data) => { | ||
| const peer = getPeer(ws, getPeers(globalPeers, ws.data.namespace), baseUtils.sync, hooks); | ||
| hooks.callHook("pong", peer, data); | ||
| } | ||
@@ -54,3 +64,3 @@ } | ||
| }; | ||
| function getPeer(ws, peers, sync) { | ||
| function getPeer(ws, peers, sync, hooks) { | ||
| if (ws.data.peer) return ws.data.peer; | ||
@@ -62,3 +72,4 @@ const peer = new BunPeer({ | ||
| namespace: ws.data.namespace, | ||
| sync | ||
| sync, | ||
| hooks | ||
| }); | ||
@@ -98,3 +109,11 @@ ws.data.peer = peer; | ||
| } | ||
| ping(data) { | ||
| try { | ||
| return this._internal.ws.ping(data); | ||
| } catch (error) { | ||
| this._internal.hooks.callHook("error", this, new WSError(error)); | ||
| return 0; | ||
| } | ||
| } | ||
| }; | ||
| export { bunAdapter as default }; |
@@ -17,3 +17,3 @@ import { AdapterHookable, adapterUtils, getPeers, toBufferLike } from "../_chunks/adapter.mjs"; | ||
| if (negotiatedProtocol) upgradeOptions.protocol = negotiatedProtocol; | ||
| if (options.idleTimeout !== void 0) upgradeOptions.idleTimeout = options.idleTimeout; | ||
| upgradeOptions.idleTimeout = options.idleTimeout ?? 30; | ||
| const { response, socket } = request.upgradeWebSocket(Object.keys(upgradeOptions).length > 0 ? upgradeOptions : void 0); | ||
@@ -20,0 +20,0 @@ const remoteAddress = request.headers.get("x-real-ip") || void 0; |
@@ -19,3 +19,4 @@ import { AdapterHookable, adapterUtils, getPeers, toBufferLike } from "../_chunks/adapter.mjs"; | ||
| headers, | ||
| protocol: headers.get("sec-websocket-protocol") ?? "" | ||
| protocol: headers.get("sec-websocket-protocol") ?? "", | ||
| idleTimeout: options.idleTimeout ?? 30 | ||
| }); | ||
@@ -22,0 +23,0 @@ const peers = getPeers(globalPeers, namespace); |
@@ -1,2 +0,2 @@ | ||
| import { Adapter, AdapterInstance, AdapterOptions, Peer, PeerContext, SyncDriver } from "../_chunks/adapter.mjs"; | ||
| import { Adapter, AdapterHookable, AdapterInstance, AdapterOptions, Peer, PeerContext, SyncDriver } from "../_chunks/adapter.mjs"; | ||
| import { WebSocket } from "../_chunks/web.mjs"; | ||
@@ -33,2 +33,3 @@ import uws from "uWebSockets.js"; | ||
| sync?: SyncDriver; | ||
| hooks: AdapterHookable; | ||
| }> { | ||
@@ -47,2 +48,3 @@ get remoteAddress(): string | undefined; | ||
| terminate(): void; | ||
| ping(data?: uws.RecognizedString): number; | ||
| } | ||
@@ -49,0 +51,0 @@ declare class UWSReqProxy extends StubRequest { |
| import { AdapterHookable, adapterUtils, getPeers, toBufferLike } from "../_chunks/adapter.mjs"; | ||
| import { Message, Peer } from "../_chunks/peer.mjs"; | ||
| import { WSError } from "../_chunks/error.mjs"; | ||
| import { StubRequest } from "../_chunks/_request.mjs"; | ||
@@ -11,6 +12,7 @@ const uwsAdapter = (options = {}) => { | ||
| websocket: { | ||
| idleTimeout: options.idleTimeout ?? 30, | ||
| ...options.uws, | ||
| close(ws, code, message) { | ||
| const peers = getPeers(globalPeers, ws.getUserData().namespace); | ||
| const peer = getPeer(ws, peers, baseUtils.sync); | ||
| const peer = getPeer(ws, peers, baseUtils.sync, hooks); | ||
| peer._internal.ws.readyState = 2; | ||
@@ -25,12 +27,22 @@ peers.delete(peer); | ||
| message(ws, message, _isBinary) { | ||
| const peer = getPeer(ws, getPeers(globalPeers, ws.getUserData().namespace), baseUtils.sync); | ||
| const peer = getPeer(ws, getPeers(globalPeers, ws.getUserData().namespace), baseUtils.sync, hooks); | ||
| hooks.callHook("message", peer, new Message(message, peer)); | ||
| }, | ||
| drain(ws) { | ||
| const peer = getPeer(ws, getPeers(globalPeers, ws.getUserData().namespace)); | ||
| const peer = getPeer(ws, getPeers(globalPeers, ws.getUserData().namespace), baseUtils.sync, hooks); | ||
| hooks.callHook("drain", peer); | ||
| }, | ||
| ping(ws, message) { | ||
| if (!hooks.options.hooks?.ping && !hooks.options.resolve) return; | ||
| const peer = getPeer(ws, getPeers(globalPeers, ws.getUserData().namespace), baseUtils.sync, hooks); | ||
| hooks.callHook("ping", peer, new Uint8Array(message)); | ||
| }, | ||
| pong(ws, message) { | ||
| if (!hooks.options.hooks?.pong && !hooks.options.resolve) return; | ||
| const peer = getPeer(ws, getPeers(globalPeers, ws.getUserData().namespace), baseUtils.sync, hooks); | ||
| hooks.callHook("pong", peer, new Uint8Array(message)); | ||
| }, | ||
| open(ws) { | ||
| const peers = getPeers(globalPeers, ws.getUserData().namespace); | ||
| const peer = getPeer(ws, peers, baseUtils.sync); | ||
| const peer = getPeer(ws, peers, baseUtils.sync, hooks); | ||
| peers.add(peer); | ||
@@ -80,3 +92,3 @@ hooks.callHook("open", peer); | ||
| }; | ||
| function getPeer(uws, peers, sync) { | ||
| function getPeer(uws, peers, sync, hooks) { | ||
| const uwsData = uws.getUserData(); | ||
@@ -91,3 +103,4 @@ if (uwsData.peer) return uwsData.peer; | ||
| uwsData, | ||
| sync | ||
| sync, | ||
| hooks | ||
| }); | ||
@@ -131,2 +144,10 @@ uwsData.peer = peer; | ||
| } | ||
| ping(data) { | ||
| try { | ||
| return this._internal.uws.ping(data); | ||
| } catch (error) { | ||
| this._internal.hooks.callHook("error", this, new WSError(error)); | ||
| return 0; | ||
| } | ||
| } | ||
| }; | ||
@@ -133,0 +154,0 @@ var UWSReqProxy = class extends StubRequest { |
| import bunAdapter from "../adapters/bun.mjs"; | ||
| import { defaultResolve } from "../_chunks/_resolve.mjs"; | ||
| import { serve as serve$1 } from "srvx/bun"; | ||
@@ -7,3 +8,3 @@ function plugin(wsOpts) { | ||
| hooks: wsOpts, | ||
| resolve: wsOpts.resolve, | ||
| resolve: defaultResolve(server, wsOpts), | ||
| ...wsOpts.options?.bun | ||
@@ -10,0 +11,0 @@ }); |
| import bunnyAdapter from "../adapters/bunny.mjs"; | ||
| import { defaultResolve } from "../_chunks/_resolve.mjs"; | ||
| import { serve as serve$1 } from "srvx/bunny"; | ||
@@ -7,3 +8,3 @@ function plugin(wsOpts) { | ||
| hooks: wsOpts, | ||
| resolve: wsOpts.resolve, | ||
| resolve: defaultResolve(server, wsOpts), | ||
| ...wsOpts.options?.bunny | ||
@@ -10,0 +11,0 @@ }); |
| import cloudflareAdapter from "../adapters/cloudflare.mjs"; | ||
| import { defaultResolve } from "../_chunks/_resolve.mjs"; | ||
| import { serve as serve$1 } from "srvx/cloudflare"; | ||
@@ -7,3 +8,3 @@ function plugin(wsOpts) { | ||
| hooks: wsOpts, | ||
| resolve: wsOpts.resolve, | ||
| resolve: defaultResolve(server, wsOpts), | ||
| ...wsOpts.options?.cloudflare | ||
@@ -10,0 +11,0 @@ }); |
| import sseAdapter from "../adapters/sse.mjs"; | ||
| import { defaultResolve } from "../_chunks/_resolve.mjs"; | ||
| import { serve as serve$1 } from "srvx"; | ||
| function plugin(wsOpts) { | ||
| const ws = sseAdapter({ | ||
| hooks: wsOpts, | ||
| resolve: wsOpts.resolve, | ||
| ...wsOpts.options?.sse | ||
| }); | ||
| console.warn("[crossws] Using SSE adapter for WebSocket support. This requires a custom WebSocket client (https://crossws.h3.dev/adapters/sse)."); | ||
| return (server) => { | ||
| const ws = sseAdapter({ | ||
| hooks: wsOpts, | ||
| resolve: defaultResolve(server, wsOpts), | ||
| ...wsOpts.options?.sse | ||
| }); | ||
| server.options.middleware.unshift((req, next) => { | ||
@@ -12,0 +13,0 @@ if (req.headers.get("upgrade")?.toLowerCase() === "websocket") return ws.fetch(req); |
| import denoAdapter from "../adapters/deno.mjs"; | ||
| import { defaultResolve } from "../_chunks/_resolve.mjs"; | ||
| import { serve as serve$1 } from "srvx/deno"; | ||
@@ -7,3 +8,3 @@ function plugin(wsOpts) { | ||
| hooks: wsOpts, | ||
| resolve: wsOpts.resolve, | ||
| resolve: defaultResolve(server, wsOpts), | ||
| ...wsOpts.options?.deno | ||
@@ -10,0 +11,0 @@ }); |
| import { nodeAdapter } from "../_chunks/node.mjs"; | ||
| import { defaultResolve } from "../_chunks/_resolve.mjs"; | ||
| import { NodeRequest, serve as serve$1 } from "srvx/node"; | ||
@@ -7,4 +8,4 @@ function plugin(wsOpts) { | ||
| hooks: wsOpts, | ||
| resolve: wsOpts.resolve, | ||
| ...wsOpts.options?.deno | ||
| resolve: defaultResolve(server, wsOpts), | ||
| ...wsOpts.options?.node | ||
| }); | ||
@@ -11,0 +12,0 @@ const originalServe = server.serve; |
+1
-1
| { | ||
| "name": "crossws", | ||
| "version": "0.4.8", | ||
| "version": "0.4.9", | ||
| "description": "Cross-platform WebSocket Servers for Node.js, Deno, Bun and Cloudflare Workers", | ||
@@ -5,0 +5,0 @@ "homepage": "https://crossws.h3.dev", |
248232
5.83%78
1.3%4773
4.63%15
50%