@@ -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.
484484Setting 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
489489const app = new HttpRouter ();
490490
491- // regular routes
491+ // register regular routes on app.get()/post()/etc
492492app .get (' /' , c => c .text (' Hello World!' ));
493493
494- // WebSocket routes
494+ // Register WebSocket routes with app.socket.at()
495495type ParamsShape = { room: string };
496496type DataShape = { user: User };
497497app .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;
553553gameRoom .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
558660And WebSockets make it super easy to create a pub-sub system with no external
@@ -563,8 +665,6 @@ import { HttpRouter } from 'bunshine';
563665
564666const app = new HttpRouter ();
565667
566- app .get (' /' , c => c .text (' Hello World!' ));
567-
568668type ParamsShape = { room: string };
569669type DataShape = { username: string };
570670app .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 },
0 commit comments