Skip to content

Commit

Permalink
Merge pull request #48 from sckv/enhance-ws
Browse files Browse the repository at this point in the history
feat(ws): add websocket handy methods exposure for easy interaction
  • Loading branch information
sckv authored Aug 1, 2021
2 parents 11e4b77 + c6469fb commit 1bc7e63
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 19 deletions.
74 changes: 65 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ yarn add barehttp
```typescript
import { BareHttp, logMe } from 'barehttp';

const app = new BareHttp({ logging: false });
const app = new BareHttp();

app.get({
route: '/route',
Expand Down Expand Up @@ -88,7 +88,7 @@ app.start((address) => {
```typescript
import { BareHttp, logMe } from 'barehttp';

const app = new BareHttp({ logging: false });
const app = new BareHttp({ logging: true });

app.get({
route:'/route',
Expand Down Expand Up @@ -186,12 +186,16 @@ Default `false`
Exposes a basic report with the routes usage under `GET /_report` route
### `BareServer.use` ((flow: BareRequest) => Promise<void> | void)
---
## `BareServer.use` ((flow: BareRequest) => Promise<void> | void)
Attach a middleware `after` the middlewares optional array.
The order of the middlewares is followed by code declarations order.
### `BareServer.get | post | patch | put | delete | options | head | declare` (Function)
---
## `BareServer.get | post | patch | put | delete | options | head | declare` (Function)
To set a route for `get | post | patch | put | delete | options | head` with following parameters:
Expand Down Expand Up @@ -219,7 +223,7 @@ app.declare({
});
```
### `BareServer.runtimeRoute.get | post | patch | put | delete | options | head | declare` (Function)
## `BareServer.runtimeRoute.get | post | patch | put | delete | options | head | declare` (Function)
Same as the above routes API, but you can only declare them when the server is `listening`
Expand All @@ -241,10 +245,6 @@ app.runtimeRoute
});
```
### `BareServer.ws` (WebSocketServer)
Refer to [external WebSocketServer](https://github.com/websockets/ws#external-https-server) for documentation.
#### `RouteOptions` (Object)
If set, provide per-route options for behavior handling
Expand All @@ -261,6 +261,54 @@ If set, provides a granular cache headers handling per route.
Request timeout value in `ms`. This will cancel the request _only_ for this route if time expired
---
## `BareServer.ws?` (WebSocketServer)
Based on `ws` package, for internals please refer to [external WebSocketServer](https://github.com/websockets/ws#external-https-server) for documentation.
This particular implementation works out easily for WebSockets interaction for pushing data to server from the clients and waiting for some answer in async.
Also exposes an way to keep pushing messages to the Client from the Server on server handle through internal clients list. (WIP optimizing this)
### `WebSocketServer.declareReceiver` ((Data, UserClient, WSClient, MessageEvent) => Promise\<M> | M)
This is the main 'handler' function for any kind of Client request. If there's a response to that push from the client the return should contain it, otherwise if the response is `void` there will be no answer to the client side.
- `Data`: is the data received from the client for this exact `Type`
- `UserClient`: is an optional client defined on the stage of `Upgrade` to provide some closured client data to be able to know what Client is exactly making the request to the Server
- `WSClient`: raw instance of `ws.Client & { userClient: UC }`
- `MessageEvent`: raw instance of `ws.MessageClient`
Code Example:
```ts
app.ws?.declareReceiver<{ ok: string }>({
type: 'BASE_TYPE',
handler: async (data, client) => {
// do your async or sync operations here
// return the response if you need to send an answer
return { cool: 'some answer', client };
},
});
```
### `WebSocketServer.defineUpgrade` ((IncomingRequest) => Promise\<M> | M)
To de able to handle authorization or any other previous operation before opening and upgrading an incoming client's request.
**If this function is not initialized with the callback, all incoming connections will be accepted by default**
```ts
app.ws?.defineUpgrade(async (req) => {
// you can do some async or sync operation here
// the returning of this function will be
// defined as the `UserClient` and attached to the `ws.Client` instance
return { access: true, client: {...properties of the client} };
});
```
---
## `BareRequest` (Class)
An instance of the request passed through to middlewares and handlers
Expand Down Expand Up @@ -332,6 +380,7 @@ Some of the features are in progress.
- [x] Request wide context storage and incorporated tracing (ready for cloud)
- [x] UID (adopted or generated)
- [x] WebSocket server exposure
- [x] handy WebSocket interaction tools, for authorization, etc.
- [x] Request-Processing-Time header and value
- [x] Promised or conventional middlewares
- [x] Logging and serialized with `pino`
Expand All @@ -342,6 +391,13 @@ Some of the features are in progress.
- [x] Request execution cancellation by timeout
- [x] Bulk/chaining routes declaration
- [x] Runtime routes hot swapping
- [ ] middlewares per route
- [ ] swagger OpenAPI 3.0 on `/docs` endpoint
- [ ] swagger OpenAPI 3.0 scheme on `/docs_raw` endpoint
- [ ] optional export of generated schema to a location (yaml, json)
- [ ] streaming/receiving of chunked multipart
- [ ] runtime validation schema generation per route response types (on project compile/on launch)
- [ ] runtime route params or query validation upon declared types (on project compile/on launch)
## Benchmarks
Expand Down
16 changes: 16 additions & 0 deletions src/__tests__/server.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ test('Enables context in the settings', async () => {
expect(data).toBeTruthy();
});

test('Automatically parses query parameters in the route call', async () => {
const app = new BareHttp();

const spyQuery = jest.fn();
app.get({
route: '/test',
handler: (flow) => spyQuery(flow.query),
});

await app.start();
await axios.get('http://localhost:3000/test?query=params&chained=ok');
await app.stop();

expect(spyQuery).toHaveBeenCalledWith({ query: 'params', chained: 'ok' });
});

test('Enables cookies decoding in the settings', async () => {
const app = new BareHttp({ cookies: true });
const spyCookies = jest.fn();
Expand Down
20 changes: 19 additions & 1 deletion src/bench/baretest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BareHttp } from '../index';

const brt = new BareHttp({ statisticsReport: true });
const brt = new BareHttp({ statisticsReport: true, ws: true });

brt.get({
route: '/myroute',
Expand All @@ -9,4 +9,22 @@ brt.get({
},
});

let clt = 0;

brt.ws?.declareReceiver<{ ok: string }>({
type: 'BASE_TYPE',
handler: async (data, client) => {
console.log({ data });
return { cool: 'some answer', client };
},
});

brt.ws?.defineUpgrade(async (req) => {
return { access: true, client: ++clt };
});

brt.ws?.handleManualConnect((ws, client) => {
console.log('connected!', ws.userClient.secId);
});

brt.start(() => console.log('server started'));
8 changes: 8 additions & 0 deletions src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CookiesManager, CookiesManagerOptions } from './middlewares/cookies/coo

import { types } from 'util';
import { Writable } from 'stream';
import url from 'url';

import type { IncomingMessage, ServerResponse } from 'http';
const generateId = hyperid();
Expand Down Expand Up @@ -44,6 +45,7 @@ const statusTuples = Object.entries(StatusCodes).reduce((acc, [name, status]) =>
export class BareRequest {
ID: { code: string };
params: { [k: string]: string | undefined } = {};
query: { [k: string]: string | undefined } = {};
remoteIp?: string;
requestBody?: any;
requestHeaders: { [key: string]: any };
Expand Down Expand Up @@ -71,6 +73,12 @@ export class BareRequest {
this.contentType = this._originalRequest.headers['content-type'] as any;
this.requestHeaders = this._originalRequest.headers;

// this is a placeholder URL base that we need to make class working
new url.URL(`http://localhost/${this._originalRequest.url}`).searchParams.forEach(
(value, name) => (this.query[name] = value),
);

// parsed;
_originalRequest['flow'] = this; // to receive flow object later on in the route handler

this.setHeaders({
Expand Down
14 changes: 7 additions & 7 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Router from 'find-my-way';
import { Server as WServer, ServerOptions } from 'ws';
import { ServerOptions } from 'ws';

import { BareRequest, CacheOpts } from './request';
import { logMe } from './logger';
Expand All @@ -14,6 +14,7 @@ import {
StatusCodesUnion,
} from './utils';
import { Cors, CorsOptions } from './middlewares/cors/cors';
import { WebSocketServer } from './websocket';

import dns from 'dns';
import { createServer, IncomingMessage, ServerResponse, Server } from 'http';
Expand Down Expand Up @@ -76,7 +77,7 @@ type BareOptions<A extends IP> = {
*/
ws?: boolean;
wsOptions?: Omit<ServerOptions, 'host' | 'port' | 'server' | 'noServer'> & {
closeHandler?: (server: WServer) => Promise<void>;
closeHandler?: (server: WebSocketServer) => Promise<void>;
};
/**
* Enable Cors
Expand Down Expand Up @@ -112,7 +113,7 @@ export type ServerMergedType = {

export class BareServer<A extends IP> {
server: Server;
ws?: WServer;
ws?: WebSocketServer;

#middlewares: Array<Middleware> = [];
#routes: Map<string, RouteReport> = new Map();
Expand Down Expand Up @@ -184,9 +185,7 @@ export class BareServer<A extends IP> {

// ws attachment
if (bo.ws) {
const wsOpts = { server: this.server };
if (bo.wsOptions) Object.assign(wsOpts, bo.wsOptions);
this.ws = new WServer(wsOpts);
this.ws = new WebSocketServer(this.server, bo.wsOptions);
}

// middlewares settings
Expand Down Expand Up @@ -361,7 +360,7 @@ export class BareServer<A extends IP> {
await this.bareOptions.wsOptions.closeHandler(this.ws);
}

this.ws.close();
this.ws._internal.close();
}

private attachGracefulHandlers() {
Expand Down Expand Up @@ -465,6 +464,7 @@ export class BareServer<A extends IP> {

start(cb?: (address: string) => void) {
this.#writeMiddlewares();
this.ws?.['_start']();
return new Promise<void>((res) =>
// https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback
this.server.listen(this.#port, this.#host, undefined, () => {
Expand Down
4 changes: 2 additions & 2 deletions src/utils/safe-json.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const JSONStringify = (data: any) => {
export const JSONStringify = (data: any): string | null => {
try {
return JSON.stringify(data);
} catch (e) {
Expand All @@ -7,7 +7,7 @@ export const JSONStringify = (data: any) => {
}
};

export const JSONParse = (data: any) => {
export const JSONParse = <R = any>(data: any): R | null => {
try {
return JSON.parse(data);
} catch (e) {
Expand Down
Loading

0 comments on commit 1bc7e63

Please sign in to comment.