spooder 4.4.11 → 4.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +54 -0
  2. package/package.json +1 -1
  3. package/src/api.ts +66 -0
package/README.md CHANGED
@@ -57,6 +57,8 @@ The `CLI` component of `spooder` is a global command-line tool for running serve
57
57
  - [`server.sse(path: string, handler: ServerSentEventHandler)`](#api-routing-server-sse)
58
58
  - [API > Routing > Webhooks](#api-routing-webhooks)
59
59
  - [`server.webhook(secret: string, path: string, handler: WebhookHandler)`](#api-routing-server-webhook)
60
+ - [API > Routing > WebSockets](#api-routing-websockets)
61
+ - [`server.websocket(path: string, handlers: WebsocketHandlers)`](#api-routing-server-websocket)
60
62
  - [API > Server Control](#api-server-control)
61
63
  - [`server.stop(immediate: boolean)`](#api-server-control-server-stop)
62
64
  - [API > Error Handling](#api-error-handling)
@@ -915,6 +917,58 @@ A webhook callback will only be called if the following critera is met by a requ
915
917
  > [!NOTE]
916
918
  > Constant-time comparison is used to prevent timing attacks when comparing the HMAC signature.
917
919
 
920
+ <a id="api-routing-websockets"></a>
921
+ ## API > Routing > WebSockets
922
+
923
+ <a id="api-routing-server-websocket"></a>
924
+ ### 🔧 `server.websocket(path: string, handlers: WebSocketHandlers)`
925
+
926
+ Register a route which handles websocket connections.
927
+
928
+ ```ts
929
+ server.websocket('/path/to/websocket', {
930
+ // all of these handlers are OPTIONAL
931
+
932
+ accept: (req) => {
933
+ // validates a request before it is upgraded
934
+ // returns HTTP 401 if FALSE is returned
935
+ // allows you to check headers/authentication
936
+ return true;
937
+ },
938
+
939
+ open: (ws) => {
940
+ // called when a websocket client connects
941
+ },
942
+
943
+ close: (ws, code, reason) => {
944
+ // called when a websocket client disconnects
945
+ },
946
+
947
+ message: (ws, message) => {
948
+ // called when a websocket message is received
949
+ // message is a string
950
+ },
951
+
952
+ message_json: (ws, data) => {
953
+ // called when a websocket message is received
954
+ // payload is parsed as JSON
955
+
956
+ // if payload cannot be parsed, socket will be
957
+ // closed with error 1003: Unsupported Data
958
+
959
+ // messages are only internally parsed if this
960
+ // handler is present
961
+ },
962
+
963
+ drain: (ws) => {
964
+ // called when a websocket with backpressure drains
965
+ }
966
+ });
967
+ ```
968
+
969
+ > [!IMPORTANT]
970
+ > While it is possible to register multiple routes for websockets, the only handler which is unique per route is `accept()`. The last handlers provided to any route (with the exception of `accept()`) will apply to ALL websocket routes. This is a limitation in Bun.
971
+
918
972
  <a id="api-server-control"></a>
919
973
  ## API > Server Control
920
974
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spooder",
3
3
  "type": "module",
4
- "version": "4.4.11",
4
+ "version": "4.5.1",
5
5
  "exports": {
6
6
  ".": {
7
7
  "bun": "./src/api.ts",
package/src/api.ts CHANGED
@@ -104,6 +104,15 @@ export async function caution(err_message_or_obj: string | object, ...err: objec
104
104
  await handle_error('caution: ', err_message_or_obj, ...err);
105
105
  }
106
106
 
107
+ type WebsocketHandlers = {
108
+ accept?: (req: Request) => boolean | Promise<boolean>,
109
+ message?: (ws: WebSocket, message: string) => void,
110
+ message_json?: (ws: WebSocket, message: JsonSerializable) => void,
111
+ open?: (ws: WebSocket) => void,
112
+ close?: (ws: WebSocket, code: number, reason: string) => void,
113
+ drain?: (ws: WebSocket) => void
114
+ };
115
+
107
116
  type CallableFunction = (...args: any[]) => any;
108
117
  type Callable = Promise<any> | CallableFunction;
109
118
 
@@ -722,6 +731,12 @@ export function serve(port: number) {
722
731
 
723
732
  const slow_requests = new WeakSet();
724
733
 
734
+ let ws_message_handler: any = undefined;
735
+ let ws_message_json_handler: any = undefined;
736
+ let ws_open_handler: any = undefined;
737
+ let ws_close_handler: any = undefined;
738
+ let ws_drain_handler: any = undefined;
739
+
725
740
  const server = Bun.serve({
726
741
  port,
727
742
  development: false,
@@ -741,6 +756,38 @@ export function serve(port: number) {
741
756
  slow_requests.delete(req);
742
757
 
743
758
  return print_request_info(req, response, url, request_time);
759
+ },
760
+
761
+ websocket: {
762
+ message(ws, message) {
763
+ ws_message_handler?.(ws, message);
764
+
765
+ if (ws_message_json_handler) {
766
+ try {
767
+ if (message instanceof ArrayBuffer)
768
+ message = new TextDecoder().decode(message);
769
+ else if (message instanceof Buffer)
770
+ message = message.toString('utf8');
771
+
772
+ const parsed = JSON.parse(message as string);
773
+ ws_message_json_handler(ws, parsed);
774
+ } catch (e) {
775
+ ws.close(1003, 'Unsupported Data');
776
+ }
777
+ }
778
+ },
779
+
780
+ open(ws) {
781
+ ws_open_handler?.(ws);
782
+ },
783
+
784
+ close(ws, code, reason) {
785
+ ws_close_handler?.(ws, code, reason);
786
+ },
787
+
788
+ drain(ws) {
789
+ ws_drain_handler?.(ws);
790
+ }
744
791
  }
745
792
  });
746
793
 
@@ -760,6 +807,25 @@ export function serve(port: number) {
760
807
  routes.push([[...path.split('/'), '*'], route_directory(path, dir, handler ?? default_directory_handler), method]);
761
808
  },
762
809
 
810
+ /** Add a route to upgrade connections to websockets. */
811
+ websocket: (path: string, handlers: WebsocketHandlers): void => {
812
+ routes.push([path.split('/'), async (req: Request, url: URL) => {
813
+ if (await handlers.accept?.(req) === false)
814
+ return 401; // Unauthorized
815
+
816
+ if (server.upgrade(req))
817
+ return 101; // Switching Protocols
818
+
819
+ return new Response('WebSocket upgrade failed', { status: 500 });
820
+ }, 'GET']);
821
+
822
+ ws_message_json_handler = handlers.message_json;
823
+ ws_open_handler = handlers.open;
824
+ ws_close_handler = handlers.close;
825
+ ws_message_handler = handlers.message;
826
+ ws_drain_handler = handlers.drain;
827
+ },
828
+
763
829
  webhook: (secret: string, path: string, handler: WebhookHandler): void => {
764
830
  routes.push([path.split('/'), async (req: Request, url: URL) => {
765
831
  if (req.headers.get('Content-Type') !== 'application/json')