Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: experimental websocket support #2170

Merged
merged 12 commits into from
Feb 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/.config/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Create web servers with everything you need and deploy them whereve
github: unjs/nitro
url: https://nitro.unjs.io
themeColor: "red"
# automd: true
automd: true
redirects:
/deploy/node: /deploy/runtimes/node
landing:
Expand Down
97 changes: 97 additions & 0 deletions docs/1.guide/8.websocket.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
icon: cib:socket-io
---

# WebSocket

> Nitro natively support a cross platform WebSocket API

Nitro natively supports runtime agnostic [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) API using [CrossWS](https://crossws.unjs.io/) and [H3 WebSocket](https://h3.unjs.io/guide/websocket).

:read-more{title="WebSocket in MDN" to="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket"}

:read-more{title="CrossWS" to="https://crossws.unjs.io/"}

> [!IMPORTANT]
> WebSockets support is currently experimental and available in [nightly channel](/guide/nightly).
> See [unjs/nitro#2171](https://github.com/unjs/nitro/issues/2171) for platform support status.

## Usage

Enable experimental flag first:

::code-group
```ts [nitro.config.ts]
export default defineNitroConfig({
experimental: {
websocket: true
}
})
```

```ts [nuxt.config.ts]
export default defineNuxtConfig({
nitro: {
experimental: {
websocket: true
}
}
})
```
::

Create a websocket handler in `routes/_ws.ts` or `server/routes/_ws.ts` for Nuxt).

<!-- automd:file code src="../../examples/websocket/routes/_ws.ts" -->

```ts [_ws.ts]
export default defineWebSocketHandler({
open(peer) {
console.log("[ws] open", peer);
},

message(peer, message) {
console.log("[ws] message", peer, message);
if (message.text().includes("ping")) {
peer.send("pong");
}
},

close(peer, event) {
console.log("[ws] close", peer, event);
},

error(peer, error) {
console.log("[ws] error", peer, error);
},
});

```

<!-- /automd -->

> [!NOTE]
> Nitro allows you defining multiple websocket handlers using same routing of event handlers.

Use a client to connect to server. Example: (`routes/websocket.ts` or `server/routes/websocket.ts` for Nuxt)

<!-- automd:file code src="../../examples/websocket/routes/index.ts" -->

```ts [index.ts]
export default defineEventHandler(() => {
return $fetch(
"https://raw.githubusercontent.com/unjs/crossws/main/examples/h3/public/index.html"
);
});

```

<!-- /automd -->

Now you can try it on `/websocket` route!

## Server Sent Events (SSE)

As an alternative to WebSockets, you can use [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)

:read-more{to="https://h3.unjs.io/guide/websocket#server-sent-events-sse" title="SSE guide in H3"}
File renamed without changes.
Binary file modified docs/bun.lockb
Binary file not shown.
5 changes: 5 additions & 0 deletions examples/websocket/nitro.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default defineNitroConfig({
experimental: {
websocket: true,
},
});
11 changes: 11 additions & 0 deletions examples/websocket/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "example-hello-world",
"private": true,
"scripts": {
"dev": "nitro dev",
"build": "nitro build"
},
"devDependencies": {
"nitropack": "latest"
}
}
20 changes: 20 additions & 0 deletions examples/websocket/routes/_ws.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default defineWebSocketHandler({
open(peer) {
console.log("[ws] open", peer);
},

message(peer, message) {
console.log("[ws] message", peer, message);
if (message.text().includes("ping")) {
peer.send("pong");
}
},

close(peer, event) {
console.log("[ws] close", peer, event);
},

error(peer, error) {
console.log("[ws] error", peer, error);
},
});
5 changes: 5 additions & 0 deletions examples/websocket/routes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default defineEventHandler(() => {
return $fetch(
"https://raw.githubusercontent.com/unjs/crossws/main/examples/h3/public/index.html"
);
});
3 changes: 3 additions & 0 deletions examples/websocket/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "./.nitro/types/tsconfig.json"
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"citty": "^0.1.6",
"consola": "^3.2.3",
"cookie-es": "^1.0.0",
"crossws": "^0.2.3",
"defu": "^6.1.4",
"destr": "^2.0.3",
"dot-prop": "^8.0.2",
Expand All @@ -87,7 +88,7 @@
"fs-extra": "^11.2.0",
"globby": "^14.0.1",
"gzip-size": "^7.0.0",
"h3": "^1.11.0",
"h3": "^1.11.1",
"hookable": "^5.5.3",
"httpxy": "^0.1.5",
"is-primitive": "^3.0.1",
Expand Down Expand Up @@ -128,6 +129,7 @@
"@azure/static-web-apps-cli": "^1.1.6",
"@cloudflare/workers-types": "^4.20240222.0",
"@types/aws-lambda": "^8.10.134",
"@types/bun": "^1.0.7",
"@types/estree": "^1.0.5",
"@types/etag": "^1.8.3",
"@types/fs-extra": "^11.0.4",
Expand Down
51 changes: 41 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export const nitroRuntimeDependencies = [
"unctx",
"unenv",
"unstorage",
"crossws",
];
11 changes: 11 additions & 0 deletions src/dev/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,17 @@ export function createDevServer(nitro: Nitro): NitroDevServer {
let listeners: Listener[] = [];
const _listen: NitroDevServer["listen"] = async (port, opts?) => {
const listener = await listen(toNodeListener(app), { port, ...opts });
listener.server.on("upgrade", (req, sock, head) => {
proxy.proxy.ws(
req,
sock as any,
{
target: getWorkerAddress(),
xfwd: true,
},
head
);
});
listeners.push(listener);
return listener;
};
Expand Down
1 change: 1 addition & 0 deletions src/rollup/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export const getRollupConfig = (nitro: Nitro): RollupConfig => {
"versions?.nitro": nitroPkg.version,
// Internal
_asyncContext: nitro.options.experimental.asyncContext,
_websocket: nitro.options.experimental.websocket,
};

// Universal import.meta
Expand Down
26 changes: 18 additions & 8 deletions src/runtime/entries/bun.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
import "#internal/nitro/virtual/polyfill";
import type {} from "bun";
import wsAdapter from "crossws/adapters/bun";
import { nitroApp } from "../app";

// @ts-expect-error: Bun global
const ws = import.meta._websocket
? wsAdapter(nitroApp.h3App.websocket)
: undefined;

const server = Bun.serve({
port: process.env.NITRO_PORT || process.env.PORT || 3000,
async fetch(request: Request) {
const url = new URL(request.url);
websocket: import.meta._websocket ? ws.websocket : undefined,
async fetch(req, server) {
if (import.meta._websocket && (await ws.handleUpgrade(req, server))) {
return;
}

const url = new URL(req.url);

let body;
if (request.body) {
body = await request.arrayBuffer();
if (req.body) {
body = await req.arrayBuffer();
}

return nitroApp.localFetch(url.pathname + url.search, {
host: url.hostname,
protocol: url.protocol,
headers: request.headers,
method: request.method,
redirect: request.redirect,
headers: req.headers,
method: req.method,
redirect: req.redirect,
body,
});
},
Expand Down
Loading