barehttp 0.3.1 → 0.4.2

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.
package/README.md CHANGED
@@ -49,7 +49,7 @@ yarn add barehttp
49
49
  ```typescript
50
50
  import { BareHttp, logMe } from 'barehttp';
51
51
 
52
- const app = new BareHttp({ logging: false });
52
+ const app = new BareHttp();
53
53
 
54
54
  app.get({
55
55
  route: '/route',
@@ -88,7 +88,7 @@ app.start((address) => {
88
88
  ```typescript
89
89
  import { BareHttp, logMe } from 'barehttp';
90
90
 
91
- const app = new BareHttp({ logging: false });
91
+ const app = new BareHttp({ logging: true });
92
92
 
93
93
  app.get({
94
94
  route:'/route',
@@ -186,12 +186,16 @@ Default `false`
186
186
 
187
187
  Exposes a basic report with the routes usage under `GET /_report` route
188
188
 
189
- ### `BareServer.use` ((flow: BareRequest) => Promise<void> | void)
189
+ ---
190
+
191
+ ## `BareServer.use` ((flow: BareRequest) => Promise<void> | void)
190
192
 
191
193
  Attach a middleware `after` the middlewares optional array.
192
194
  The order of the middlewares is followed by code declarations order.
193
195
 
194
- ### `BareServer.get | post | patch | put | delete | options | head | declare` (Function)
196
+ ---
197
+
198
+ ## `BareServer.get | post | patch | put | delete | options | head | declare` (Function)
195
199
 
196
200
  To set a route for `get | post | patch | put | delete | options | head` with following parameters:
197
201
 
@@ -219,7 +223,7 @@ app.declare({
219
223
  });
220
224
  ```
221
225
 
222
- ### `BareServer.runtimeRoute.get | post | patch | put | delete | options | head | declare` (Function)
226
+ ## `BareServer.runtimeRoute.get | post | patch | put | delete | options | head | declare` (Function)
223
227
 
224
228
  Same as the above routes API, but you can only declare them when the server is `listening`
225
229
 
@@ -241,10 +245,6 @@ app.runtimeRoute
241
245
  });
242
246
  ```
243
247
 
244
- ### `BareServer.ws` (WebSocketServer)
245
-
246
- Refer to [external WebSocketServer](https://github.com/websockets/ws#external-https-server) for documentation.
247
-
248
248
  #### `RouteOptions` (Object)
249
249
 
250
250
  If set, provide per-route options for behavior handling
@@ -261,6 +261,54 @@ If set, provides a granular cache headers handling per route.
261
261
 
262
262
  Request timeout value in `ms`. This will cancel the request _only_ for this route if time expired
263
263
 
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
+
264
312
  ## `BareRequest` (Class)
265
313
 
266
314
  An instance of the request passed through to middlewares and handlers
@@ -297,13 +345,21 @@ Imperatively disables cache, does the same as `disableCache: true` in `RouteOpti
297
345
 
298
346
  Imperatively sets the cache, does the same as `cache: CacheOptions` in `RouteOptions`
299
347
 
348
+ ### `addHeader` (Function)
349
+
350
+ Adds a header outgoing header string as a (key, value) `addHeader(header, value)`. Can **not** overwrite.
351
+
352
+ ### `addHeaders` (Function)
353
+
354
+ Adds outgoing headers in a "batch", merges provided headers object `{ [header: string]: value }` to already existing headers. Can **not** overwrite.
355
+
300
356
  ### `setHeader` (Function)
301
357
 
302
- Set outgoing header as a (key, value) arguments `setHeader(header, value)`. Can overwrite
358
+ Does the same as `addHeader` but overrides the value.
303
359
 
304
360
  ### `setHeaders` (Function)
305
361
 
306
- Set outgoing headers in a "batch", merges provided headers object `{ [header: string]: value }` to already existing headers. Can overwrite
362
+ Does the same as `addHeaders` but overrides the value.
307
363
 
308
364
  ### `status` (Function)
309
365
 
@@ -332,6 +388,7 @@ Some of the features are in progress.
332
388
  - [x] Request wide context storage and incorporated tracing (ready for cloud)
333
389
  - [x] UID (adopted or generated)
334
390
  - [x] WebSocket server exposure
391
+ - [x] handy WebSocket interaction tools, for authorization, etc.
335
392
  - [x] Request-Processing-Time header and value
336
393
  - [x] Promised or conventional middlewares
337
394
  - [x] Logging and serialized with `pino`
@@ -342,6 +399,13 @@ Some of the features are in progress.
342
399
  - [x] Request execution cancellation by timeout
343
400
  - [x] Bulk/chaining routes declaration
344
401
  - [x] Runtime routes hot swapping
402
+ - [ ] middlewares per route
403
+ - [ ] swagger OpenAPI 3.0 on `/docs` endpoint
404
+ - [ ] swagger OpenAPI 3.0 scheme on `/docs_raw` endpoint
405
+ - [ ] optional export of generated schema to a location (yaml, json)
406
+ - [ ] streaming/receiving of chunked multipart
407
+ - [ ] runtime validation schema generation per route response types (on project compile/on launch)
408
+ - [ ] runtime route params or query validation upon declared types (on project compile/on launch)
345
409
 
346
410
  ## Benchmarks
347
411
 
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Execution = void 0;
7
7
  const hyperid_1 = __importDefault(require("hyperid"));
8
- const generateId = hyperid_1.default();
8
+ const generateId = (0, hyperid_1.default)();
9
9
  class Execution {
10
10
  id;
11
11
  type;
@@ -23,7 +23,7 @@ exports.logMe = exports.logHttp = void 0;
23
23
  const pino_1 = __importStar(require("pino"));
24
24
  const serializers_1 = require("./serializers");
25
25
  const env_1 = require("../env");
26
- const asyncDest = env_1.envs.isProd ? [pino_1.destination({ sync: false })] : [];
26
+ const asyncDest = env_1.envs.isProd ? [(0, pino_1.destination)({ sync: false })] : [];
27
27
  const pinoCommonOptions = {
28
28
  timestamp: () => `,"time":"${new Date()[env_1.envs.isProd ? 'toISOString' : 'toLocaleTimeString']()}"`,
29
29
  formatters: {
@@ -32,36 +32,18 @@ const pinoCommonOptions = {
32
32
  messageKey: 'message',
33
33
  prettyPrint: !env_1.envs.isProd,
34
34
  };
35
- const logger = pino_1.default(pinoCommonOptions, asyncDest[0]);
36
- if (env_1.envs.isProd) {
37
- setInterval(function () {
38
- logger.flush();
39
- }, 10000).unref();
40
- const handler = pino_1.default.final(logger, (err, finalLogger, evt) => {
41
- finalLogger.info(`${evt} caught`);
42
- if (err)
43
- finalLogger.error(err, 'error caused exit');
44
- process.exit(err ? 1 : 0);
45
- });
46
- // catch all the ways node might exit
47
- process.on('beforeExit', () => handler(null, 'beforeExit'));
48
- process.on('exit', () => handler(null, 'exit'));
49
- process.on('uncaughtException', (err) => handler(err, 'uncaughtException'));
50
- process.on('SIGINT', () => handler(null, 'SIGINT'));
51
- process.on('SIGQUIT', () => handler(null, 'SIGQUIT'));
52
- process.on('SIGTERM', () => handler(null, 'SIGTERM'));
53
- }
35
+ const logger = (0, pino_1.default)(pinoCommonOptions, asyncDest[0]);
54
36
  const logHttp = (...params) => {
55
- const { level, logObject } = serializers_1.serializeHttp(...params);
37
+ const { level, logObject } = (0, serializers_1.serializeHttp)(...params);
56
38
  logger[level](logObject);
57
39
  };
58
40
  exports.logHttp = logHttp;
59
41
  // TODO: remove the test condition
60
42
  exports.logMe = {
61
- debug: (...args) => !env_1.envs.isTest && logger.debug(serializers_1.serializeLog(...args)),
62
- info: (...args) => !env_1.envs.isTest && logger.info(serializers_1.serializeLog(...args)),
63
- warn: (...args) => !env_1.envs.isTest && logger.warn(serializers_1.serializeLog(...args)),
64
- error: (...args) => !env_1.envs.isTest && logger.error(serializers_1.serializeLog(...args)),
65
- fatal: (...args) => !env_1.envs.isTest && logger.fatal(serializers_1.serializeLog(...args)),
66
- trace: (...args) => !env_1.envs.isTest && logger.trace(serializers_1.serializeLog(...args)),
43
+ debug: (...args) => !env_1.envs.isTest && logger.debug((0, serializers_1.serializeLog)(...args)),
44
+ info: (...args) => !env_1.envs.isTest && logger.info((0, serializers_1.serializeLog)(...args)),
45
+ warn: (...args) => !env_1.envs.isTest && logger.warn((0, serializers_1.serializeLog)(...args)),
46
+ error: (...args) => !env_1.envs.isTest && logger.error((0, serializers_1.serializeLog)(...args)),
47
+ fatal: (...args) => !env_1.envs.isTest && logger.fatal((0, serializers_1.serializeLog)(...args)),
48
+ trace: (...args) => !env_1.envs.isTest && logger.trace((0, serializers_1.serializeLog)(...args)),
67
49
  };
@@ -26,7 +26,7 @@ const parseArgs = (argSlice) => argSlice.map((arg) => {
26
26
  return arg;
27
27
  });
28
28
  function serializeLog(...args) {
29
- const site = callsites_1.default()[2];
29
+ const site = (0, callsites_1.default)()[2];
30
30
  const meta = {
31
31
  timestamp: Date.now(),
32
32
  location: `${site.getFileName()}:${site.getLineNumber()}:${site.getColumnNumber()}`,
@@ -16,7 +16,7 @@ class CookiesManager {
16
16
  this.flow = flow;
17
17
  const secret = this.options.secret || '';
18
18
  const enableRotation = Array.isArray(secret);
19
- this.signer = typeof secret === 'string' || enableRotation ? signer_1.secretsOperator(secret) : null;
19
+ this.signer = typeof secret === 'string' || enableRotation ? (0, signer_1.secretsOperator)(secret) : null;
20
20
  }
21
21
  setCookie(name, value, options, signer) {
22
22
  const localSigner = signer || this.signer;
package/lib/request.d.ts CHANGED
@@ -23,6 +23,9 @@ export declare class BareRequest {
23
23
  params: {
24
24
  [k: string]: string | undefined;
25
25
  };
26
+ query: {
27
+ [k: string]: string | undefined;
28
+ };
26
29
  remoteIp?: string;
27
30
  requestBody?: any;
28
31
  requestHeaders: {
@@ -32,22 +35,24 @@ export declare class BareRequest {
32
35
  cm?: CookiesManager;
33
36
  sent: boolean;
34
37
  private cache;
35
- private startTime;
38
+ private startTime?;
36
39
  private startDate;
37
40
  private remoteClient;
38
- private countTimeFormat;
41
+ private requestTimeFormat?;
39
42
  private headers;
40
43
  private cookies;
41
44
  private contentType?;
42
45
  private timeout?;
43
- constructor(_originalRequest: IncomingMessage, _originalResponse: ServerResponse, logging?: boolean);
46
+ constructor(_originalRequest: IncomingMessage, _originalResponse: ServerResponse, options?: {
47
+ logging?: boolean;
48
+ requestTimeFormat?: 'ms' | 's';
49
+ });
44
50
  private readBody;
45
51
  private attachCookieManager;
46
52
  private populateCookies;
47
53
  private classifyRequestBody;
48
54
  private setRemoteClient;
49
55
  private setRequestTime;
50
- private setTimeFormat;
51
56
  private cleanHeader;
52
57
  private attachTimeout;
53
58
  private setParams;
@@ -58,10 +63,14 @@ export declare class BareRequest {
58
63
  };
59
64
  disableCache(): void;
60
65
  setCache(cacheOpts: CacheOpts): void;
66
+ addHeader(header: string, value: string | number | string[] | number[]): void;
61
67
  setHeader(header: string, value: string | number | string[] | number[]): void;
62
68
  setHeaders(headers: {
63
69
  [header: string]: string | number | string[] | number[];
64
70
  }): void;
71
+ addHeaders(headers: {
72
+ [header: string]: string | number | string[] | number[];
73
+ }): void;
65
74
  status(status: StatusCodesUnion): this;
66
75
  sendStatus(status: StatusCodesUnion): void;
67
76
  stream<T extends NodeJS.WritableStream>(stream: T): void;
package/lib/request.js CHANGED
@@ -11,7 +11,8 @@ const logger_1 = require("./logger");
11
11
  const cookie_manager_1 = require("./middlewares/cookies/cookie-manager");
12
12
  const util_1 = require("util");
13
13
  const stream_1 = require("stream");
14
- const generateId = hyperid_1.default();
14
+ const url_1 = __importDefault(require("url"));
15
+ const generateId = (0, hyperid_1.default)();
15
16
  const statusTuples = Object.entries(utils_1.StatusCodes).reduce((acc, [name, status]) => {
16
17
  acc[status] = utils_1.StatusPhrases[name];
17
18
  return acc;
@@ -21,6 +22,7 @@ class BareRequest {
21
22
  _originalResponse;
22
23
  ID;
23
24
  params = {};
25
+ query = {};
24
26
  remoteIp;
25
27
  requestBody;
26
28
  requestHeaders;
@@ -31,24 +33,33 @@ class BareRequest {
31
33
  startTime;
32
34
  startDate = new Date();
33
35
  remoteClient = '';
34
- countTimeFormat = 's';
36
+ requestTimeFormat;
35
37
  headers = {};
36
38
  cookies = {};
37
39
  contentType;
38
40
  timeout;
39
- constructor(_originalRequest, _originalResponse, logging) {
41
+ constructor(_originalRequest, _originalResponse, options) {
40
42
  this._originalRequest = _originalRequest;
41
43
  this._originalResponse = _originalResponse;
42
44
  this.ID = { code: _originalRequest.headers['x-request-id'] || generateId() };
43
45
  this.remoteIp = _originalRequest.socket.remoteAddress;
44
46
  this.contentType = this._originalRequest.headers['content-type'];
45
47
  this.requestHeaders = this._originalRequest.headers;
46
- _originalRequest['flow'] = this; // to receive an id later on in the route handler
47
- this.setHeaders({ 'Content-Type': 'text/plain', 'X-Request-Id': this.ID.code });
48
- this.startTime = process.hrtime();
48
+ // this is a placeholder URL base that we need to make class working
49
+ new url_1.default.URL(`http://localhost/${this._originalRequest.url}`).searchParams.forEach((value, name) => (this.query[name] = value));
50
+ // parsed;
51
+ _originalRequest['flow'] = this; // to receive flow object later on in the route handler
52
+ this.addHeaders({
53
+ 'Content-Type': 'text/plain; charset=utf-8',
54
+ 'X-Request-Id': this.ID.code,
55
+ });
56
+ if (options?.requestTimeFormat) {
57
+ this.startTime = process.hrtime();
58
+ this.requestTimeFormat = options.requestTimeFormat;
59
+ }
49
60
  // call logging section
50
- if (logging === true) {
51
- _originalResponse.on('close', () => logger_1.logHttp(this.headers, this.startDate, this.remoteClient, _originalRequest, _originalResponse));
61
+ if (options?.logging === true) {
62
+ _originalResponse.on('close', () => (0, logger_1.logHttp)(this.headers, this.startDate, this.remoteClient, _originalRequest, _originalResponse));
52
63
  }
53
64
  }
54
65
  readBody() {
@@ -79,7 +90,7 @@ class BareRequest {
79
90
  case 'text/plain':
80
91
  return wholeChunk.toString();
81
92
  case 'application/json':
82
- return safe_json_1.JSONParse(wholeChunk.toString());
93
+ return (0, safe_json_1.JSONParse)(wholeChunk.toString());
83
94
  case 'application/x-www-form-urlencoded':
84
95
  const store = {};
85
96
  for (const curr of wholeChunk.toString().split('&')) {
@@ -97,17 +108,16 @@ class BareRequest {
97
108
  this.remoteClient = remoteClient;
98
109
  }
99
110
  setRequestTime() {
111
+ if (!this.requestTimeFormat)
112
+ return;
100
113
  const diff = process.hrtime(this.startTime);
101
- const time = diff[0] * (this.countTimeFormat === 's' ? 1 : 1e3) +
102
- diff[1] * (this.countTimeFormat === 's' ? 1e-9 : 1e-6);
114
+ const time = diff[0] * (this.requestTimeFormat === 's' ? 1 : 1e3) +
115
+ diff[1] * (this.requestTimeFormat === 's' ? 1e-9 : 1e-6);
103
116
  this.setHeaders({
104
117
  'X-Processing-Time': time,
105
- 'X-Processing-Time-Mode': this.countTimeFormat === 's' ? 'seconds' : 'milliseconds',
118
+ 'X-Processing-Time-Mode': this.requestTimeFormat === 's' ? 'seconds' : 'milliseconds',
106
119
  });
107
120
  }
108
- setTimeFormat(format) {
109
- this.countTimeFormat = format;
110
- }
111
121
  cleanHeader(header) {
112
122
  delete this.headers[header];
113
123
  }
@@ -148,7 +158,7 @@ class BareRequest {
148
158
  if (cacheHeader.length > 0)
149
159
  this.setHeader(directive, cacheHeader);
150
160
  }
151
- setHeader(header, value) {
161
+ addHeader(header, value) {
152
162
  const old = this.headers[header];
153
163
  const parsedVal = Array.isArray(value) ? value.join(', ') : '' + value;
154
164
  if (old) {
@@ -158,11 +168,20 @@ class BareRequest {
158
168
  this.headers[header] = parsedVal;
159
169
  }
160
170
  }
171
+ setHeader(header, value) {
172
+ const parsedVal = Array.isArray(value) ? value.join(', ') : '' + value;
173
+ this.headers[header] = parsedVal;
174
+ }
161
175
  setHeaders(headers) {
162
176
  for (const [header, value] of Object.entries(headers)) {
163
177
  this.setHeader(header, value);
164
178
  }
165
179
  }
180
+ addHeaders(headers) {
181
+ for (const [header, value] of Object.entries(headers)) {
182
+ this.addHeader(header, value);
183
+ }
184
+ }
166
185
  status(status) {
167
186
  this.statusToSend = status;
168
187
  return this;
@@ -175,7 +194,7 @@ class BareRequest {
175
194
  }
176
195
  json(data) {
177
196
  // to generate with fast-json-stringify schema issue #1
178
- const jsoned = safe_json_1.JSONStringify(data);
197
+ const jsoned = (0, safe_json_1.JSONStringify)(data);
179
198
  this.setHeader('Content-Type', 'application/json');
180
199
  this._send(jsoned ? jsoned : undefined);
181
200
  }
@@ -184,7 +203,7 @@ class BareRequest {
184
203
  logger_1.logMe.error("Tying to send into closed client's stream");
185
204
  return;
186
205
  }
187
- if (this._originalResponse.headersSent) {
206
+ if (this._originalResponse.headersSent || this.sent) {
188
207
  logger_1.logMe.error('Trying to send with the headers already sent');
189
208
  return;
190
209
  }
package/lib/server.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /// <reference types="node" />
2
- import { Server as WServer, ServerOptions } from 'ws';
2
+ import { ServerOptions } from 'ws';
3
3
  import { BareRequest, CacheOpts } from './request';
4
4
  import { CookiesManagerOptions } from './middlewares/cookies/cookie-manager';
5
5
  import { HttpMethodsUnion, StatusCodesUnion } from './utils';
6
6
  import { CorsOptions } from './middlewares/cors/cors';
7
+ import { WebSocketServer } from './websocket';
7
8
  import { Server } from 'http';
8
9
  declare type Middleware = (flow: BareRequest) => Promise<void> | void;
9
10
  declare type Handler = (flow: BareRequest) => any;
@@ -38,7 +39,7 @@ declare type BareOptions<A extends IP> = {
38
39
  errorHandlerMiddleware?: ErrorHandler;
39
40
  /**
40
41
  * Request time format in `seconds` or `milliseconds`
41
- * Default 's' - seconds
42
+ * Default - disabled
42
43
  */
43
44
  requestTimeFormat?: 's' | 'ms';
44
45
  /**
@@ -61,7 +62,7 @@ declare type BareOptions<A extends IP> = {
61
62
  */
62
63
  ws?: boolean;
63
64
  wsOptions?: Omit<ServerOptions, 'host' | 'port' | 'server' | 'noServer'> & {
64
- closeHandler?: (server: WServer) => Promise<void>;
65
+ closeHandler?: (server: WebSocketServer) => Promise<void>;
65
66
  };
66
67
  /**
67
68
  * Enable Cors
@@ -96,7 +97,7 @@ export declare class BareServer<A extends IP> {
96
97
  #private;
97
98
  private bareOptions;
98
99
  server: Server;
99
- ws?: WServer;
100
+ ws?: WebSocketServer;
100
101
  constructor(bareOptions?: BareOptions<A>);
101
102
  private mainOptionsSetter;
102
103
  private applyMiddlewares;
package/lib/server.js CHANGED
@@ -5,13 +5,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.BareHttp = exports.BareServer = void 0;
7
7
  const find_my_way_1 = __importDefault(require("find-my-way"));
8
- const ws_1 = require("ws");
9
8
  const request_1 = require("./request");
10
9
  const logger_1 = require("./logger");
11
10
  const context_1 = require("./context");
12
11
  const report_1 = require("./report");
13
12
  const utils_1 = require("./utils");
14
13
  const cors_1 = require("./middlewares/cors/cors");
14
+ const websocket_1 = require("./websocket");
15
15
  const dns_1 = __importDefault(require("dns"));
16
16
  const http_1 = require("http");
17
17
  class BareServer {
@@ -21,7 +21,7 @@ class BareServer {
21
21
  #middlewares = [];
22
22
  #routes = new Map();
23
23
  #routesLib = new Map();
24
- #router = find_my_way_1.default({ ignoreTrailingSlash: true });
24
+ #router = (0, find_my_way_1.default)({ ignoreTrailingSlash: true });
25
25
  #errorHandler = this.basicErrorHandler;
26
26
  #corsInstance;
27
27
  #port = 3000;
@@ -30,7 +30,7 @@ class BareServer {
30
30
  constructor(bareOptions = {}) {
31
31
  this.bareOptions = bareOptions;
32
32
  // init
33
- this.server = http_1.createServer(this.#listener.bind(this));
33
+ this.server = (0, http_1.createServer)(this.#listener.bind(this));
34
34
  this.attachGracefulHandlers();
35
35
  this.attachRoutesDeclarator();
36
36
  this.mainOptionsSetter();
@@ -38,14 +38,12 @@ class BareServer {
38
38
  }
39
39
  #listener = (request, response) => {
40
40
  const { requestTimeFormat, logging } = this.bareOptions;
41
- const flow = new request_1.BareRequest(request, response, logging);
41
+ const flow = new request_1.BareRequest(request, response, { logging, requestTimeFormat });
42
42
  // init and attach request uuid to the context
43
43
  if (this.bareOptions.context) {
44
- context_1.newContext('request');
44
+ (0, context_1.newContext)('request');
45
45
  context_1.context.current?.store.set('id', flow.ID.code);
46
46
  }
47
- if (requestTimeFormat)
48
- flow['setTimeFormat'](requestTimeFormat);
49
47
  // attach a flow to the flow memory storage
50
48
  this.applyMiddlewares(flow).catch((e) => this.#errorHandler(e, flow, 400));
51
49
  };
@@ -73,13 +71,10 @@ class BareServer {
73
71
  this.#host = typeof bo.serverAddress === 'string' ? bo.serverAddress : '0.0.0.0';
74
72
  // context setting
75
73
  if (bo.context)
76
- context_1.enableContext();
74
+ (0, context_1.enableContext)();
77
75
  // ws attachment
78
76
  if (bo.ws) {
79
- const wsOpts = { server: this.server };
80
- if (bo.wsOptions)
81
- Object.assign(wsOpts, bo.wsOptions);
82
- this.ws = new ws_1.Server(wsOpts);
77
+ this.ws = new websocket_1.WebSocketServer(this.server, bo.wsOptions);
83
78
  }
84
79
  // middlewares settings
85
80
  if (bo.errorHandlerMiddleware) {
@@ -155,8 +150,8 @@ class BareServer {
155
150
  }
156
151
  registerReport() {
157
152
  this.setRoute('GET', '/_report', false, (flow) => {
158
- flow.setHeader('content-type', 'text/html');
159
- flow.send(report_1.generateReport(this.#routes));
153
+ flow.setHeader('Content-Type', 'text/html');
154
+ flow.send((0, report_1.generateReport)(this.#routes));
160
155
  });
161
156
  }
162
157
  handleRoute(req, routeParams, handle, encodedRoute, opts) {
@@ -217,7 +212,7 @@ class BareServer {
217
212
  if (this.bareOptions.wsOptions?.closeHandler) {
218
213
  await this.bareOptions.wsOptions.closeHandler(this.ws);
219
214
  }
220
- this.ws.close();
215
+ this.ws._internal.close();
221
216
  }
222
217
  attachGracefulHandlers() {
223
218
  const graceful = async (code = 0) => {
@@ -285,6 +280,7 @@ class BareServer {
285
280
  }
286
281
  start(cb) {
287
282
  this.#writeMiddlewares();
283
+ this.ws?.['_start']();
288
284
  return new Promise((res) =>
289
285
  // https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback
290
286
  this.server.listen(this.#port, this.#host, undefined, () => {
@@ -1,2 +1,2 @@
1
1
  export declare const JSONStringify: (data: any) => string | null;
2
- export declare const JSONParse: (data: any) => any;
2
+ export declare const JSONParse: <R = any>(data: any) => R | null;
@@ -0,0 +1,37 @@
1
+ /// <reference types="node" />
2
+ import Client, { MessageEvent, Server as WServer, ServerOptions } from 'ws';
3
+ import { IncomingMessage, Server } from 'http';
4
+ declare type UserClient = {
5
+ secId: string;
6
+ [k: string]: any;
7
+ };
8
+ declare type AuthAccess<T> = {
9
+ access: boolean;
10
+ message?: string;
11
+ client?: T;
12
+ };
13
+ export declare type WsMessageHandler<D = any, UC extends UserClient = UserClient, M = any> = (data: D, client: UC, _ws: ClientWS<UC>, _event: MessageEvent) => Promise<M> | M;
14
+ declare type ClientWS<UC extends UserClient = UserClient> = Client & {
15
+ userClient: UC;
16
+ };
17
+ export declare class WebSocketServer {
18
+ #private;
19
+ private opts;
20
+ _internal: WServer;
21
+ private customUpgradeDone;
22
+ constructor(server: Server, opts?: ServerOptions);
23
+ private _start;
24
+ defineUpgrade<T>(fn: (request: IncomingMessage) => Promise<AuthAccess<T>> | AuthAccess<T>): void;
25
+ private doUpgrade;
26
+ private rejectUpgrade;
27
+ private attachTypesHandling;
28
+ private send;
29
+ declareReceiver<D = any, C extends UserClient = UserClient>(receiver: {
30
+ type: string;
31
+ handler: WsMessageHandler<D, C>;
32
+ }): void;
33
+ getClientById<T extends UserClient>(id: string): ClientWS<T> | undefined;
34
+ getClientByCriteria<T extends UserClient>(criteria: string, value: any, criteriaFunction?: (client: ClientWS<T>) => boolean): Client | undefined;
35
+ handleManualConnect<T extends UserClient>(fn: (socket: ClientWS<T>, client: T) => Promise<void> | void): void;
36
+ }
37
+ export {};
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.WebSocketServer = void 0;
7
+ const ws_1 = require("ws");
8
+ const callsites_1 = __importDefault(require("callsites"));
9
+ const hyperid_1 = __importDefault(require("hyperid"));
10
+ const logger_1 = require("./logger");
11
+ const utils_1 = require("./utils");
12
+ const generateId = (0, hyperid_1.default)();
13
+ class WebSocketServer {
14
+ opts;
15
+ _internal;
16
+ #httpServer;
17
+ #types = new Map();
18
+ customUpgradeDone = false;
19
+ constructor(server, opts = {}) {
20
+ this.opts = opts;
21
+ this.#httpServer = server;
22
+ }
23
+ _start() {
24
+ if (!this._internal) {
25
+ this.#createWServer({ server: this.#httpServer });
26
+ }
27
+ }
28
+ #createWServer(newOptions = {}) {
29
+ const opts = Object.assign({}, this.opts, newOptions);
30
+ this._internal = new ws_1.Server(opts);
31
+ this.attachTypesHandling();
32
+ }
33
+ defineUpgrade(fn) {
34
+ if (this.customUpgradeDone) {
35
+ throw new Error('Cannot redeclare again a custom upgrade.');
36
+ }
37
+ const newOptions = Object.assign({}, this.opts, { noServer: true });
38
+ this.#createWServer(newOptions);
39
+ this.#httpServer.on('upgrade', (request, socket, head) => {
40
+ try {
41
+ const response = fn(request);
42
+ if (response instanceof Promise) {
43
+ response
44
+ .then((answer) => this.doUpgrade(answer, request, socket, head))
45
+ .catch((e) => this.rejectUpgrade(request, socket, e?.message, e));
46
+ }
47
+ else {
48
+ this.doUpgrade(response, request, socket, head);
49
+ }
50
+ }
51
+ catch (e) {
52
+ this.rejectUpgrade(request, socket, e?.message, e);
53
+ }
54
+ });
55
+ this.customUpgradeDone = true;
56
+ }
57
+ doUpgrade(answer, request, socket, head) {
58
+ if (!answer.access)
59
+ this.rejectUpgrade(request, socket, answer.message);
60
+ else {
61
+ this._internal.handleUpgrade(request, socket, head, (ws) => {
62
+ const userClient = {
63
+ secId: request.headers['sec-websocket-key'] || generateId(),
64
+ ...(answer.client || {}),
65
+ };
66
+ ws.userClient = userClient;
67
+ this._internal.emit('connection', ws, request, userClient);
68
+ });
69
+ }
70
+ }
71
+ rejectUpgrade(request, socket, message = 'Not Authorized', data) {
72
+ logger_1.logMe.warn(message || `Upgrade rejected for the client from ${request.socket.remoteAddress}`, data);
73
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); // TODO: enhance to be able to personalize this
74
+ socket.destroy();
75
+ }
76
+ attachTypesHandling() {
77
+ this._internal.on('connection', (ws, _, client) => {
78
+ ws.onmessage = (event) => {
79
+ const decode = (0, utils_1.JSONParse)(event.data);
80
+ if (decode === null) {
81
+ logger_1.logMe.error('Incorrect data received from the client', {
82
+ data: event.data,
83
+ client: client ?? 'UNDEFINED_CLIENT',
84
+ });
85
+ }
86
+ else if (!decode.type) {
87
+ logger_1.logMe.error(`Data from the client does not contain 'type' field`, {
88
+ data: event.data,
89
+ client: client ?? 'UNDEFINED_CLIENT',
90
+ });
91
+ }
92
+ else {
93
+ const procedure = this.#types.get(decode.type);
94
+ if (!procedure || typeof procedure.handler !== 'function') {
95
+ logger_1.logMe.error(`There's no correct procedure for type "${decode.type}"`, {
96
+ data: event.data,
97
+ client: client ?? 'UNDEFINED_CLIENT',
98
+ });
99
+ }
100
+ else {
101
+ try {
102
+ const response = procedure.handler(decode, client, ws, event);
103
+ if (response instanceof Promise) {
104
+ response
105
+ .then((resolvedResponse) => {
106
+ if (!resolvedResponse)
107
+ return;
108
+ this.send({ ws, client }, (0, utils_1.JSONStringify)({ type: `${decode.type}_RESPONSE`, ...resolvedResponse }));
109
+ })
110
+ .catch((e) => logger_1.logMe.error(`Error working out a handler for type ${decode.type}`, {
111
+ error: e,
112
+ client,
113
+ data: decode,
114
+ }));
115
+ }
116
+ else {
117
+ if (!response)
118
+ return;
119
+ this.send({ ws, client }, (0, utils_1.JSONStringify)({ type: `${decode.type}_RESPONSE`, ...response }));
120
+ }
121
+ }
122
+ catch (e) {
123
+ logger_1.logMe.error(`Error working out a handler for type ${decode.type}`, {
124
+ error: e,
125
+ client,
126
+ data: decode,
127
+ });
128
+ }
129
+ }
130
+ }
131
+ };
132
+ });
133
+ }
134
+ send(ctx, data) {
135
+ if (ctx.ws.readyState === ws_1.OPEN) {
136
+ ctx.ws.send(data);
137
+ }
138
+ else {
139
+ logger_1.logMe.error('Could not send data for the client', { client: ctx.client });
140
+ }
141
+ }
142
+ declareReceiver(receiver) {
143
+ const previousDeclaration = this.#types.get(receiver.type);
144
+ if (previousDeclaration) {
145
+ throw new Error(`Can not redeclare a type ${receiver.type} for the WS Server, already declared at ${previousDeclaration.loc}`);
146
+ }
147
+ if (typeof receiver.handler !== 'function') {
148
+ throw new Error(`Can't declare a handler with type ${typeof receiver.handler}, should be a function with following signature: WsMessageHandler<T,?>`);
149
+ }
150
+ const place = (0, callsites_1.default)()[2];
151
+ const loc = `${place.getFileName()}:${place.getLineNumber()}:${place.getColumnNumber()}`;
152
+ this.#types.set(receiver.type, { loc, handler: receiver.handler });
153
+ }
154
+ getClientById(id) {
155
+ for (const client of this._internal.clients.values()) {
156
+ if (client.userClient.secId === id) {
157
+ return client;
158
+ }
159
+ }
160
+ }
161
+ getClientByCriteria(criteria, value, criteriaFunction) {
162
+ for (const client of this._internal.clients.values()) {
163
+ if (typeof criteriaFunction === 'function') {
164
+ if (criteriaFunction(client)) {
165
+ return client;
166
+ }
167
+ }
168
+ if (client.userClient[criteria] === value) {
169
+ return client;
170
+ }
171
+ }
172
+ }
173
+ handleManualConnect(fn) {
174
+ this._internal.on('connection', (ws, _, client) => fn(ws, client));
175
+ }
176
+ }
177
+ exports.WebSocketServer = WebSocketServer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "barehttp",
3
- "version": "0.3.1",
3
+ "version": "0.4.2",
4
4
  "description": "Lightweight and fast Node.js web server",
5
5
  "main": "lib/index.js",
6
6
  "directories": {
@@ -15,7 +15,7 @@
15
15
  "build:dev": "rm -rf ./dev-lib && tsc -p tsconfig.dev.json",
16
16
  "test": "jest --runInBand --coverage",
17
17
  "lint": "eslint ./src --fix",
18
- "release": "semantic-release"
18
+ "release": "semantic-release -e ./.releaserc.json"
19
19
  },
20
20
  "keywords": [
21
21
  "nodejs",
@@ -36,34 +36,36 @@
36
36
  "callsites": "^3.1.0",
37
37
  "cookie": "^0.4.1",
38
38
  "cookie-signature": "^1.1.0",
39
- "find-my-way": "^4.3.0",
40
- "hyperid": "^2.1.0",
41
- "pino": "^6.11.3",
42
- "pino-pretty": "^5.0.2",
43
- "ws": "^7.4.6"
39
+ "find-my-way": "^5.0.0",
40
+ "hyperid": "^2.3.1",
41
+ "pino": "^7.5.1",
42
+ "pino-pretty": "^7.2.0",
43
+ "ws": "^8.3.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@ts-morph/bootstrap": "^0.9.1",
47
- "@types/cookie": "^0.4.0",
46
+ "@semantic-release/git": "^10.0.1",
47
+ "@semantic-release/github": "^8.0.2",
48
+ "@ts-morph/bootstrap": "^0.12.2",
49
+ "@types/cookie": "^0.4.1",
48
50
  "@types/cookie-signature": "^1.0.3",
49
- "@types/jest": "^26.0.23",
50
- "@types/node": "^15.0.2",
51
- "@types/pino": "^6.3.8",
52
- "@types/ws": "^7.4.7",
53
- "@typescript-eslint/eslint-plugin": "^4.22.1",
54
- "@typescript-eslint/parser": "^4.22.1",
51
+ "@types/jest": "^27.0.3",
52
+ "@types/node": "^14.17.0",
53
+ "@types/pino": "^7.0.5",
54
+ "@types/ws": "^8.2.1",
55
+ "@typescript-eslint/eslint-plugin": "^5.5.0",
56
+ "@typescript-eslint/parser": "^5.5.0",
55
57
  "axios": "^0.21.1",
56
- "eslint": "^7.26.0",
57
- "eslint-plugin-import": "^2.22.1",
58
- "eslint-plugin-jest": "^24.3.6",
58
+ "eslint": "^8.3.0",
59
+ "eslint-plugin-import": "^2.25.3",
60
+ "eslint-plugin-jest": "^25.3.0",
59
61
  "express": "^4.17.1",
60
- "fastify": "^3.15.1",
61
- "jest": "^26.6.3",
62
- "semantic-release": "^17.4.2",
62
+ "fastify": "^3.24.1",
63
+ "jest": "^27.4.3",
64
+ "semantic-release": "^18.0.1",
63
65
  "supertest": "^6.1.3",
64
- "ts-jest": "^26.5.6",
66
+ "ts-jest": "^27.0.7",
65
67
  "ts-node-dev": "^1.1.6",
66
- "typescript": "^4.3.2"
68
+ "typescript": "^4.5.2"
67
69
  },
68
70
  "optionalDependencies": {
69
71
  "bufferutil": "^4.0.3",