Skip to content

Commit d816797

Browse files
authored
Merge pull request #48 from sckv/enhance-ws
feat(ws): add websocket handy methods exposure for easy interaction
2 parents bf5ed4b + 9f8ab80 commit d816797

7 files changed

Lines changed: 338 additions & 19 deletions

File tree

README.md

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ yarn add barehttp
4949
```typescript
5050
import { BareHttp, logMe } from 'barehttp';
5151

52-
const app = new BareHttp({ logging: false });
52+
const app = new BareHttp();
5353

5454
app.get({
5555
route: '/route',
@@ -88,7 +88,7 @@ app.start((address) => {
8888
```typescript
8989
import { BareHttp, logMe } from 'barehttp';
9090

91-
const app = new BareHttp({ logging: false });
91+
const app = new BareHttp({ logging: true });
9292

9393
app.get({
9494
route:'/route',
@@ -186,12 +186,16 @@ Default `false`
186186
187187
Exposes a basic report with the routes usage under `GET /_report` route
188188
189-
### `BareServer.use` ((flow: BareRequest) => Promise<void> | void)
189+
---
190+
191+
## `BareServer.use` ((flow: BareRequest) => Promise<void> | void)
190192
191193
Attach a middleware `after` the middlewares optional array.
192194
The order of the middlewares is followed by code declarations order.
193195
194-
### `BareServer.get | post | patch | put | delete | options | head | declare` (Function)
196+
---
197+
198+
## `BareServer.get | post | patch | put | delete | options | head | declare` (Function)
195199
196200
To set a route for `get | post | patch | put | delete | options | head` with following parameters:
197201
@@ -219,7 +223,7 @@ app.declare({
219223
});
220224
```
221225
222-
### `BareServer.runtimeRoute.get | post | patch | put | delete | options | head | declare` (Function)
226+
## `BareServer.runtimeRoute.get | post | patch | put | delete | options | head | declare` (Function)
223227
224228
Same as the above routes API, but you can only declare them when the server is `listening`
225229
@@ -241,10 +245,6 @@ app.runtimeRoute
241245
});
242246
```
243247
244-
### `BareServer.ws` (WebSocketServer)
245-
246-
Refer to [external WebSocketServer](https://github.com/websockets/ws#external-https-server) for documentation.
247-
248248
#### `RouteOptions` (Object)
249249
250250
If set, provide per-route options for behavior handling
@@ -261,6 +261,54 @@ If set, provides a granular cache headers handling per route.
261261
262262
Request timeout value in `ms`. This will cancel the request _only_ for this route if time expired
263263
264+
---
265+
266+
## `BareServer.ws?` (WebSocketServer)
267+
268+
Based on `ws` package, for internals please refer to [external WebSocketServer](https://github.com/websockets/ws#external-https-server) for documentation.
269+
270+
This particular implementation works out easily for WebSockets interaction for pushing data to server from the clients and waiting for some answer in async.
271+
272+
Also exposes an way to keep pushing messages to the Client from the Server on server handle through internal clients list. (WIP optimizing this)
273+
274+
### `WebSocketServer.declareReceiver` ((Data, UserClient, WSClient, MessageEvent) => Promise\<M> | M)
275+
276+
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.
277+
278+
- `Data`: is the data received from the client for this exact `Type`
279+
- `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
280+
- `WSClient`: raw instance of `ws.Client & { userClient: UC }`
281+
- `MessageEvent`: raw instance of `ws.MessageClient`
282+
283+
Code Example:
284+
285+
```ts
286+
app.ws?.declareReceiver<{ ok: string }>({
287+
type: 'BASE_TYPE',
288+
handler: async (data, client) => {
289+
// do your async or sync operations here
290+
// return the response if you need to send an answer
291+
return { cool: 'some answer', client };
292+
},
293+
});
294+
```
295+
296+
### `WebSocketServer.defineUpgrade` ((IncomingRequest) => Promise\<M> | M)
297+
298+
To de able to handle authorization or any other previous operation before opening and upgrading an incoming client's request.
299+
**If this function is not initialized with the callback, all incoming connections will be accepted by default**
300+
301+
```ts
302+
app.ws?.defineUpgrade(async (req) => {
303+
// you can do some async or sync operation here
304+
// the returning of this function will be
305+
// defined as the `UserClient` and attached to the `ws.Client` instance
306+
return { access: true, client: {...properties of the client} };
307+
});
308+
```
309+
310+
---
311+
264312
## `BareRequest` (Class)
265313
266314
An instance of the request passed through to middlewares and handlers
@@ -332,6 +380,7 @@ Some of the features are in progress.
332380
- [x] Request wide context storage and incorporated tracing (ready for cloud)
333381
- [x] UID (adopted or generated)
334382
- [x] WebSocket server exposure
383+
- [x] handy WebSocket interaction tools, for authorization, etc.
335384
- [x] Request-Processing-Time header and value
336385
- [x] Promised or conventional middlewares
337386
- [x] Logging and serialized with `pino`
@@ -342,6 +391,13 @@ Some of the features are in progress.
342391
- [x] Request execution cancellation by timeout
343392
- [x] Bulk/chaining routes declaration
344393
- [x] Runtime routes hot swapping
394+
- [ ] middlewares per route
395+
- [ ] swagger OpenAPI 3.0 on `/docs` endpoint
396+
- [ ] swagger OpenAPI 3.0 scheme on `/docs_raw` endpoint
397+
- [ ] optional export of generated schema to a location (yaml, json)
398+
- [ ] streaming/receiving of chunked multipart
399+
- [ ] runtime validation schema generation per route response types (on project compile/on launch)
400+
- [ ] runtime route params or query validation upon declared types (on project compile/on launch)
345401
346402
## Benchmarks
347403

src/__tests__/server.integration.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,22 @@ test('Enables context in the settings', async () => {
6161
expect(data).toBeTruthy();
6262
});
6363

64+
test('Automatically parses query parameters in the route call', async () => {
65+
const app = new BareHttp();
66+
67+
const spyQuery = jest.fn();
68+
app.get({
69+
route: '/test',
70+
handler: (flow) => spyQuery(flow.query),
71+
});
72+
73+
await app.start();
74+
await axios.get('http://localhost:3000/test?query=params&chained=ok');
75+
await app.stop();
76+
77+
expect(spyQuery).toHaveBeenCalledWith({ query: 'params', chained: 'ok' });
78+
});
79+
6480
test('Enables cookies decoding in the settings', async () => {
6581
const app = new BareHttp({ cookies: true });
6682
const spyCookies = jest.fn();

src/bench/baretest.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BareHttp } from '../index';
22

3-
const brt = new BareHttp({ statisticsReport: true });
3+
const brt = new BareHttp({ statisticsReport: true, ws: true });
44

55
brt.get({
66
route: '/myroute',
@@ -9,4 +9,22 @@ brt.get({
99
},
1010
});
1111

12+
let clt = 0;
13+
14+
brt.ws?.declareReceiver<{ ok: string }>({
15+
type: 'BASE_TYPE',
16+
handler: async (data, client) => {
17+
console.log({ data });
18+
return { cool: 'some answer', client };
19+
},
20+
});
21+
22+
brt.ws?.defineUpgrade(async (req) => {
23+
return { access: true, client: ++clt };
24+
});
25+
26+
brt.ws?.handleManualConnect((ws, client) => {
27+
console.log('connected!', ws.userClient.secId);
28+
});
29+
1230
brt.start(() => console.log('server started'));

src/request.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { CookiesManager, CookiesManagerOptions } from './middlewares/cookies/coo
88

99
import { types } from 'util';
1010
import { Writable } from 'stream';
11+
import url from 'url';
1112

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

76+
// this is a placeholder URL base that we need to make class working
77+
new url.URL(`http://localhost/${this._originalRequest.url}`).searchParams.forEach(
78+
(value, name) => (this.query[name] = value),
79+
);
80+
81+
// parsed;
7482
_originalRequest['flow'] = this; // to receive flow object later on in the route handler
7583

7684
this.setHeaders({

src/server.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Router from 'find-my-way';
2-
import { Server as WServer, ServerOptions } from 'ws';
2+
import { ServerOptions } from 'ws';
33

44
import { BareRequest, CacheOpts } from './request';
55
import { logMe } from './logger';
@@ -14,6 +14,7 @@ import {
1414
StatusCodesUnion,
1515
} from './utils';
1616
import { Cors, CorsOptions } from './middlewares/cors/cors';
17+
import { WebSocketServer } from './websocket';
1718

1819
import dns from 'dns';
1920
import { createServer, IncomingMessage, ServerResponse, Server } from 'http';
@@ -76,7 +77,7 @@ type BareOptions<A extends IP> = {
7677
*/
7778
ws?: boolean;
7879
wsOptions?: Omit<ServerOptions, 'host' | 'port' | 'server' | 'noServer'> & {
79-
closeHandler?: (server: WServer) => Promise<void>;
80+
closeHandler?: (server: WebSocketServer) => Promise<void>;
8081
};
8182
/**
8283
* Enable Cors
@@ -112,7 +113,7 @@ export type ServerMergedType = {
112113

113114
export class BareServer<A extends IP> {
114115
server: Server;
115-
ws?: WServer;
116+
ws?: WebSocketServer;
116117

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

185186
// ws attachment
186187
if (bo.ws) {
187-
const wsOpts = { server: this.server };
188-
if (bo.wsOptions) Object.assign(wsOpts, bo.wsOptions);
189-
this.ws = new WServer(wsOpts);
188+
this.ws = new WebSocketServer(this.server, bo.wsOptions);
190189
}
191190

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

364-
this.ws.close();
363+
this.ws._internal.close();
365364
}
366365

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

466465
start(cb?: (address: string) => void) {
467466
this.#writeMiddlewares();
467+
this.ws?.['_start']();
468468
return new Promise<void>((res) =>
469469
// https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback
470470
this.server.listen(this.#port, this.#host, undefined, () => {

src/utils/safe-json.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const JSONStringify = (data: any) => {
1+
export const JSONStringify = (data: any): string | null => {
22
try {
33
return JSON.stringify(data);
44
} catch (e) {
@@ -7,7 +7,7 @@ export const JSONStringify = (data: any) => {
77
}
88
};
99

10-
export const JSONParse = (data: any) => {
10+
export const JSONParse = <R = any>(data: any): R | null => {
1111
try {
1212
return JSON.parse(data);
1313
} catch (e) {

0 commit comments

Comments
 (0)