Skip to content

Commit cf0620d

Browse files
committed
more documentation
1 parent deaad84 commit cf0620d

File tree

2 files changed

+127
-26
lines changed

2 files changed

+127
-26
lines changed

packages/bunshine/README.md

Lines changed: 126 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -183,20 +183,20 @@ app.get('/hello', (c: Context, next: NextFunction) => {
183183
});
184184

185185
// Or create Response objects with convenience functions:
186-
return c.json(data, init);
187-
return c.text(text, init);
188-
return c.js(jsText, init);
189-
return c.xml(xmlText, init);
190-
return c.html(htmlText, init);
191-
return c.css(cssText, init);
192-
return c.file(pathOrSource, init);
193-
194-
// above init is ResponseInit:
195-
{
196-
headers: Headers | Record<string, string> | Array<[string, string]>;
197-
status: number;
198-
statusText: string;
199-
}
186+
return c.json(data, init); // data to pass to JSON.stringify
187+
return c.text(text, init); // plain text
188+
return c.js(jsText, init); // plain-text js
189+
return c.xml(xmlText, init); // plain-text xml
190+
return c.html(htmlText, init); // plain-text html
191+
return c.css(cssText, init); // plain-text css
192+
return c.file(pathOrSource, init); // file path, BunFile or binary source
193+
194+
// above init is the Web Standards ResponseInit:
195+
type ResponseInit = {
196+
headers?: Headers | Record<string, string> | Array<[string, string]>;
197+
status?: number;
198+
statusText?: string;
199+
};
200200

201201
// Create a redirect Response:
202202
return c.redirect(url, status); // status defaults to 302 (Temporary Redirect)
@@ -484,21 +484,21 @@ to subsequent middleware such as loggers.
484484
Setting up websockets at various paths is easy with the `socket` property.
485485

486486
```ts
487-
import { HttpRouter } from 'bunshine';
487+
import { HttpRouter, SocketMessage } from 'bunshine';
488488

489489
const app = new HttpRouter();
490490

491-
// regular routes
491+
// register regular routes on app.get()/post()/etc
492492
app.get('/', c => c.text('Hello World!'));
493493

494-
// WebSocket routes
494+
// Register WebSocket routes with app.socket.at()
495495
type ParamsShape = { room: string };
496496
type DataShape = { user: User };
497497
app.socket.at<ParmasShape, DataShape>('/games/rooms/:room', {
498498
// Optional. Allows you to specify arbitrary data to attach to ws.data.
499-
upgrade: sc => {
499+
upgrade: c => {
500500
// upgrade is called on first connection, before HTTP 101 is issued
501-
const cookies = sc.request.headers.get('cookie');
501+
const cookies = c.request.headers.get('cookie');
502502
const user = getUserFromCookies(cookies);
503503
return { user };
504504
},
@@ -525,7 +525,7 @@ app.socket.at<ParmasShape, DataShape>('/games/rooms/:room', {
525525
},
526526
// Optional. Called when the client disconnects
527527
// List of codes and messages: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code
528-
close(sc, code, message) {
528+
close(sc, code, reason) {
529529
const room = sc.params.room;
530530
const user = sc.data.user;
531531
markUserExit(room, user);
@@ -553,6 +553,108 @@ gameRoom.onerror = handleGameError;
553553
gameRoom.send(JSON.stringify({ type: 'GameMove', move: 'rock' }));
554554
```
555555

556+
### Are sockets magical?
557+
558+
They're simpler than you think. What Bun does internally:
559+
560+
- Responds to a regular HTTP request with HTTP 101 (Switching Protocols)
561+
- Tells the client to make socket connection on another port
562+
- Keeps connection open and sends/receives messages
563+
- Keeps objects in memory to represent each connected client
564+
- Tracks topic subscription and un-subscription for each client
565+
566+
In Node, the process is complicated because you have to import and orchestrate http/https, and net.
567+
But Bun provides Bun.serve() which handles everything. Bunshine is a wrapper
568+
around Bun.serve that adds routing and convenience functions.
569+
570+
With Bunshine you don't need to use Socket.IO or any other framework.
571+
Connecting from the client requires no library either. You can simply
572+
create a new WebSocket() object and use it to listen and send data.
573+
574+
For pub-sub, Bun internally handles subscriptions and broadcasts. See below for
575+
pub-sub examples using Bunshine.
576+
577+
### Socket Context properties
578+
579+
```ts
580+
import { Context, HttpRouter, NextFunction, SocketContext } from 'bunshine';
581+
582+
const app = new HttpRouter();
583+
584+
// WebSocket routes
585+
type ParamsShape = { room: string };
586+
type DataShape = { user: User };
587+
app.socket.at<ParmasShape, DataShape>('/games/rooms/:room', {
588+
// Optional. Allows you to specify arbitrary data to attach to ws.data.
589+
upgrade: (c: Context, next: NextFunction) => {
590+
// Functions are type annotated for illustrations, but is not neccessary
591+
// c and next here are the same as regular http endpoings
592+
return data; // data returned becomes available on sc.data
593+
},
594+
// Optional. Called when the client sends a message
595+
message(sc, message) {
596+
sc.url; // the URL object for the request
597+
sc.server; // Same as app.server
598+
sc.params; // Route params for the request (in this case { room: string; })
599+
sc.data; // Any data you return from the upgrade handler
600+
sc.remoteAddress; // the client or load balancer IP Address
601+
sc.readyState; // 0=connecting, 1=connected, 2=closing, 3=close
602+
sc.binaryType; // nodebuffer, arraybuffer, uint8array
603+
sc.send(message, compress /*optional*/); // compress is optional
604+
// message can be string, data to be JSON.stringified, or binary data such as Buffer or Uint8Array.
605+
// compress can be true to compress message
606+
sc.close(status /*optional*/, reason /*optional*/); // status and reason are optional
607+
// status can be a valid WebSocket status number (in the 1000s)
608+
// reason can be text to tell client why you are closing
609+
sc.terminate(); // terminates socket without telling client why
610+
sc.subscribe(topic); // The name of a topic to subscribe this client
611+
sc.unsubscribe(topic); // Name of topic to unsubscribe
612+
sc.isSubscribed(topic); // True if subscribed to that topic name
613+
sc.cork(topic); // Normally there is no reason to use this function
614+
sc.publish(topic, message, compress /*optional*/); // Publish message to all subscribed clients
615+
sc.ping(data /*optional*/); // Tell client to stay connected
616+
sc.pong(data /*optional*/); // Way to respond to client request to stay connected
617+
618+
message.raw(); // get the raw string or Buffer of the message
619+
message.text(encoding /*optional*/); // get message as string
620+
`${message}`; // will do the same as .text()
621+
message.buffer(); // get data as Buffer
622+
message.arrayBuffer(); // get data as array buffer
623+
message.readableString(); // get data as a ReadableString object
624+
message.json(); // parse data with JSON.parse()
625+
},
626+
// called when a handler throws any error
627+
error: (sc: SocketContext, error: Error) => {
628+
// sc is the same as above
629+
},
630+
// Optional. Called when the client disconnects
631+
// List of codes and messages: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code
632+
close(sc: SocketContext, code: number, reason: string) {
633+
// sc is the same as above
634+
},
635+
});
636+
637+
// start the server
638+
app.listen({ port: 3100, reusePort: true });
639+
640+
//
641+
// Browser side:
642+
//
643+
const gameRoom = new WebSocket('ws://localhost:3100/games/rooms/1?user=42');
644+
gameRoom.onmessage = e => {
645+
// receiving messages
646+
const data = JSON.parse(e.data);
647+
if (data.type === 'GameState') {
648+
setGameState(data);
649+
} else if (data.type === 'GameMove') {
650+
playMove(data);
651+
}
652+
};
653+
gameRoom.onerror = handleGameError;
654+
// send message to server
655+
gameRoom.send(JSON.stringify({ type: 'GameMove', move: 'rock' }));
656+
```
657+
556658
## WebSocket pub-sub
557659

558660
And WebSockets make it super easy to create a pub-sub system with no external
@@ -563,8 +665,6 @@ import { HttpRouter } from 'bunshine';
563665

564666
const app = new HttpRouter();
565667

566-
app.get('/', c => c.text('Hello World!'));
567-
568668
type ParamsShape = { room: string };
569669
type DataShape = { username: string };
570670
app.socket.at<ParamsShape, DataShape>('/chat/:room', {
@@ -581,11 +681,12 @@ app.socket.at<ParamsShape, DataShape>('/chat/:room', {
581681
message(sc, message) {
582682
// the server re-broadcasts incoming messages
583683
// to each connection's message handler
584-
const fullMessage = `${sc.data.username}: ${message}`;
684+
// so you need to call sc.publish() and sc.send()
685+
const fullMessage = `${sc.data.username}: ${message.text()}`;
585686
sc.publish(`chat-room-${sc.params.room}`, fullMessage);
586687
sc.send(fullMessage);
587688
},
588-
close(sc, code, message) {
689+
close(sc, code, reason) {
589690
const msg = `${sc.data.username} has left the chat`;
590691
sc.publish(`chat-room-${sc.params.room}`, msg);
591692
sc.unsubscribe(`chat-room-${sc.params.room}`);
@@ -1219,7 +1320,7 @@ app.socket.at<{ room: string }, { user: User }>('/games/rooms/:room', {
12191320
// TypeScript knows that ws.data.params.room is a string
12201321
// TypeScript knows that ws.data.user is a User
12211322
},
1222-
close(ws, code, message) {
1323+
close(ws, code, reason) {
12231324
// TypeScript knows that ws.data.params.room is a string
12241325
// TypeScript knows that ws.data.user is a User
12251326
},

packages/bunshine/src/SocketRouter/SocketContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export class SocketMessage<T extends SocketEventName> {
147147
return this.buffer().buffer;
148148
}
149149
readableStream(chunkSize: number = 1024) {
150-
// @ts-expect-error
150+
// @ts-expect-error - type is incorrect
151151
return new Blob([this.buffer()]).stream(chunkSize);
152152
}
153153
json() {

0 commit comments

Comments
 (0)