@radatek/microserver 2.1.0 → 2.2.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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * MicroServer
3
- * @version 2.1.0
3
+ * @version 2.2.1
4
4
  * @package @radatek/microserver
5
5
  * @copyright Darius Kisonas 2022
6
6
  * @license MIT
@@ -40,8 +40,9 @@ export type Routes = () => {
40
40
  export declare abstract class Plugin {
41
41
  name?: string;
42
42
  priority?: number;
43
- handler?(req: ServerRequest, res: ServerResponse, next: Function): void;
44
- routes?: () => Routes | Routes;
43
+ handler?(req: ServerRequest, res: ServerResponse, next: Function): Promise<string | object | void> | string | object | void;
44
+ routes?(): Promise<Routes> | Routes;
45
+ initialise?(): Promise<void> | void;
45
46
  constructor(router: Router, ...args: any);
46
47
  }
47
48
  interface PluginClass {
@@ -222,6 +223,14 @@ export interface Middleware {
222
223
  priority?: number;
223
224
  plugin?: Plugin;
224
225
  }
226
+ declare class Waiter {
227
+ get busy(): boolean;
228
+ startJob(): void;
229
+ endJob(id?: string): void;
230
+ get nextId(): string;
231
+ wait(id: string): Promise<void>;
232
+ resolve(id: string): void;
233
+ }
225
234
  /** Router */
226
235
  export declare class Router extends EventEmitter {
227
236
  server: MicroServer;
@@ -229,8 +238,11 @@ export declare class Router extends EventEmitter {
229
238
  plugins: {
230
239
  [key: string]: Plugin;
231
240
  };
241
+ _waiter: Waiter;
232
242
  /** @param {MicroServer} server */
233
243
  constructor(server: MicroServer);
244
+ /** bind middleware or create one from string like: 'redirect:302,https://redirect.to', 'error:422', 'param:name=value', 'acl:users/get', 'model:User', 'group:Users', 'user:admin' */
245
+ bind(fn: string | Function | object): Function;
234
246
  /** Handler */
235
247
  handler(req: ServerRequest, res: ServerResponse, next: Function, method?: string): any;
236
248
  /** Clear routes and middlewares */
@@ -241,54 +253,55 @@ export declare class Router extends EventEmitter {
241
253
  *
242
254
  * @signature add(plugin: Plugin)
243
255
  * @param {Plugin} plugin plugin module instance
244
- * @return {Router} current router
256
+ * @return {Promise<>}
245
257
  *
246
258
  * @signature add(pluginid: string, ...args: any)
247
259
  * @param {string} pluginid pluginid module
248
260
  * @param {...any} args arguments passed to constructor
249
- * @return {Router} current router
261
+ * @return {Promise<>}
250
262
  *
251
263
  * @signature add(pluginClass: typeof Plugin, ...args: any)
252
264
  * @param {typeof Plugin} pluginClass plugin class
253
265
  * @param {...any} args arguments passed to constructor
254
- * @return {Router} current router
266
+ * @return {Promise<>}
255
267
  *
256
268
  * @signature add(middleware: Middleware)
257
269
  * @param {Middleware} middleware
258
- * @return {Router} current router
270
+ * @return {Promise<>}
259
271
  *
260
272
  * @signature add(methodUrl: string, ...middlewares: any)
261
273
  * @param {string} methodUrl 'METHOD /url' or '/url'
262
274
  * @param {...any} middlewares
263
- * @return {Router} current router
275
+ * @return {Promise<>}
264
276
  *
265
277
  * @signature add(methodUrl: string, controllerClass: typeof Controller)
266
278
  * @param {string} methodUrl 'METHOD /url' or '/url'
267
279
  * @param {typeof Controller} controllerClass
268
- * @return {Router} current router
280
+ * @return {Promise<>}
269
281
  *
270
282
  * @signature add(methodUrl: string, routes: Array<Array<any>>)
271
283
  * @param {string} methodUrl 'METHOD /url' or '/url'
272
284
  * @param {Array<Array<any>>} routes list with subroutes: ['METHOD /suburl', ...middlewares]
273
- * @return {Router} current router
285
+ * @return {Promise<>}
274
286
  *
275
287
  * @signature add(methodUrl: string, routes: Array<Array<any>>)
276
288
  * @param {string} methodUrl 'METHOD /url' or '/url'
277
289
  * @param {Array<Array<any>>} routes list with subroutes: ['METHOD /suburl', ...middlewares]
278
- * @return {Router} current router
290
+ * @return {Promise<>}
279
291
  *
280
292
  * @signature add(routes: { [key: string]: Array<any> })
281
293
  * @param { {[key: string]: Array<any>} } routes list with subroutes: 'METHOD /suburl': [...middlewares]
282
- * @return {Router} current router
294
+ * @return {Promise<>}
283
295
  *
284
296
  * @signature add(methodUrl: string, routes: { [key: string]: Array<any> })
285
297
  * @param {string} methodUrl 'METHOD /url' or '/url'
286
298
  * @param { {[key: string]: Array<any>} } routes list with subroutes: 'METHOD /suburl': [...middlewares]
287
- * @return {Router} current router
299
+ * @return {Promise<>}
288
300
  */
289
- use(...args: any): Router;
301
+ use(...args: any): Promise<void>;
302
+ waitPlugin(id: string): Promise<Plugin>;
290
303
  /** Add hook */
291
- hook(url: string, ...mid: Middleware[]): Router;
304
+ hook(url: string, ...mid: Middleware[]): void;
292
305
  /** Check if middleware allready added */
293
306
  has(mid: Middleware): boolean;
294
307
  }
@@ -364,23 +377,22 @@ export declare class MicroServer extends EventEmitter {
364
377
  sockets: Set<net.Socket>;
365
378
  /** server instances */
366
379
  servers: Set<net.Server>;
380
+ _waiter: Waiter;
367
381
  static plugins: {
368
382
  [key: string]: PluginClass;
369
383
  };
370
- get plugins(): {
371
- [key: string]: Plugin;
372
- };
373
384
  constructor(config: MicroServerConfig);
374
385
  /** Add one time listener or call immediatelly for 'ready' */
375
386
  once(name: string, cb: Function): this;
376
387
  /** Add listener and call immediatelly for 'ready' */
377
388
  on(name: string, cb: Function): this;
389
+ isReady(): boolean;
390
+ waitReady(): Promise<void>;
391
+ waitPlugin(id: string): Promise<void>;
378
392
  /** Listen server, should be used only if config.listen is not set */
379
- listen(config?: ListenConfig): Promise<unknown>;
380
- /** bind middleware or create one from string like: 'redirect:302,https://redirect.to', 'error:422', 'param:name=value', 'acl:users/get', 'model:User', 'group:Users', 'user:admin' */
381
- bind(fn: string | Function | object): Function;
393
+ listen(config?: ListenConfig): Promise<void>;
382
394
  /** Add middleware, routes, etc.. see {router.use} */
383
- use(...args: any): MicroServer;
395
+ use(...args: any): Promise<void>;
384
396
  /** Default server handler */
385
397
  handler(req: ServerRequest, res: ServerResponse): void;
386
398
  protected requestInit(req: ServerRequest, res?: ServerResponse): void;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * MicroServer
3
- * @version 2.1.0
3
+ * @version 2.2.1
4
4
  * @package @radatek/microserver
5
5
  * @copyright Darius Kisonas 2022
6
6
  * @license MIT
@@ -21,6 +21,9 @@ const defaultExpire = 24 * 60 * 60;
21
21
  const defaultMaxBodySize = 5 * 1024 * 1024;
22
22
  const defaultMethods = 'HEAD,GET,POST,PUT,PATCH,DELETE';
23
23
  function NOOP(...args) { }
24
+ function isFunction(fn) {
25
+ return typeof fn === 'function' && !fn.prototype?.constructor;
26
+ }
24
27
  export class Warning extends Error {
25
28
  constructor(text) {
26
29
  super(text);
@@ -71,8 +74,9 @@ export class ServerRequest extends http.IncomingMessage {
71
74
  _init(router) {
72
75
  Object.assign(this, {
73
76
  router,
77
+ auth: router.auth,
74
78
  protocol: 'encrypted' in this.socket && this.socket.encrypted ? 'https' : 'http',
75
- get: {},
79
+ query: {},
76
80
  params: {},
77
81
  paramsList: [],
78
82
  path: '/',
@@ -692,7 +696,7 @@ export class WebSocket extends EventEmitter {
692
696
  headers = [
693
697
  `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}`,
694
698
  'Connection: close',
695
- 'Content-Type: text/html',
699
+ 'Content-Type: ' + message.startsWith('<') ? 'text/html' : 'text/plain',
696
700
  `Content-Length: ${Buffer.byteLength(message)}`,
697
701
  '',
698
702
  message
@@ -903,6 +907,48 @@ export class Controller {
903
907
  return routes;
904
908
  }
905
909
  }
910
+ class Waiter {
911
+ constructor() {
912
+ this._waiters = {};
913
+ this._id = 0;
914
+ this._busy = 0;
915
+ }
916
+ get busy() {
917
+ return this._busy > 0;
918
+ }
919
+ startJob() {
920
+ this._busy++;
921
+ }
922
+ endJob(id) {
923
+ this._busy--;
924
+ if (!this._busy)
925
+ this.resolve(id || 'ready');
926
+ }
927
+ get nextId() {
928
+ return (++this._id).toString();
929
+ }
930
+ async wait(id) {
931
+ return new Promise(resolve => (this._waiters[id] = this._waiters[id] || []).push(resolve));
932
+ }
933
+ resolve(id) {
934
+ const resolvers = this._waiters[id];
935
+ if (resolvers)
936
+ for (const resolve of resolvers)
937
+ resolve(undefined);
938
+ delete this._waiters[id];
939
+ }
940
+ }
941
+ class EmitterWaiter extends Waiter {
942
+ constructor(emitter) {
943
+ super();
944
+ this._emitter = emitter;
945
+ }
946
+ resolve(id) {
947
+ if (id === 'ready')
948
+ this._emitter.emit(id);
949
+ super.resolve(id);
950
+ }
951
+ }
906
952
  /** Router */
907
953
  export class Router extends EventEmitter {
908
954
  /** @param {MicroServer} server */
@@ -912,8 +958,95 @@ export class Router extends EventEmitter {
912
958
  this._stack = [];
913
959
  this._stackAfter = [];
914
960
  this._tree = {};
961
+ this._waiter = new Waiter();
915
962
  this.server = server;
916
963
  }
964
+ /** bind middleware or create one from string like: 'redirect:302,https://redirect.to', 'error:422', 'param:name=value', 'acl:users/get', 'model:User', 'group:Users', 'user:admin' */
965
+ bind(fn) {
966
+ if (typeof fn === 'string') {
967
+ let name = fn;
968
+ let idx = name.indexOf(':');
969
+ if (idx < 0 && name.includes('=')) {
970
+ name = 'param:' + name;
971
+ idx = 5;
972
+ }
973
+ if (idx >= 0) {
974
+ const v = name.slice(idx + 1);
975
+ const type = name.slice(0, idx);
976
+ // predefined middlewares
977
+ switch (type) {
978
+ // redirect:302,https://redirect.to
979
+ case 'redirect': {
980
+ let redirect = v.split(','), code = parseInt(v[0]);
981
+ if (!code || code < 301 || code > 399)
982
+ code = 302;
983
+ return (req, res) => res.redirect(code, redirect[1] || v);
984
+ }
985
+ // error:422
986
+ case 'error':
987
+ return (req, res) => res.error(parseInt(v) || 422);
988
+ // param:name=value
989
+ case 'param': {
990
+ idx = v.indexOf('=');
991
+ if (idx > 0) {
992
+ const prm = v.slice(0, idx), val = v.slice(idx + 1);
993
+ return (req, res, next) => { req.params[prm] = val; return next(); };
994
+ }
995
+ break;
996
+ }
997
+ case 'model': {
998
+ const model = v;
999
+ return (req, res) => {
1000
+ res.isJson = true;
1001
+ req.params.model = model;
1002
+ req.model = Model.models[model];
1003
+ if (!req.model) {
1004
+ console.error(`Data model ${model} not defined for request ${req.path}`);
1005
+ return res.error(422);
1006
+ }
1007
+ return req.model.handler(req, res);
1008
+ };
1009
+ }
1010
+ // user:userid
1011
+ // group:user_groupid
1012
+ // acl:validacl
1013
+ case 'user':
1014
+ case 'group':
1015
+ case 'acl':
1016
+ return (req, res, next) => {
1017
+ if (type === 'user' && v === req.user?.id)
1018
+ return next();
1019
+ if (type === 'acl') {
1020
+ req.params.acl = v;
1021
+ if (req.auth?.acl(v))
1022
+ return next();
1023
+ }
1024
+ if (type === 'group') {
1025
+ req.params.group = v;
1026
+ if (req.user?.group === v)
1027
+ return next();
1028
+ }
1029
+ const accept = req.headers.accept || '';
1030
+ if (!res.isJson && req.auth?.options.redirect && req.method === 'GET' && !accept.includes('json') && (accept.includes('html') || accept.includes('*/*'))) {
1031
+ if (req.auth.options.redirect && req.url !== req.auth.options.redirect)
1032
+ return res.redirect(302, req.auth.options.redirect);
1033
+ else if (req.auth.options.mode !== 'cookie') {
1034
+ res.setHeader('WWW-Authenticate', `Basic realm="${req.auth.options.realm}"`);
1035
+ return res.error(401);
1036
+ }
1037
+ }
1038
+ return res.error('Permission denied');
1039
+ };
1040
+ }
1041
+ }
1042
+ throw new Error('Invalid option: ' + name);
1043
+ }
1044
+ if (fn && typeof fn === 'object' && 'handler' in fn && typeof fn.handler === 'function')
1045
+ return fn.handler.bind(fn);
1046
+ if (typeof fn !== 'function')
1047
+ throw new Error('Invalid middleware: ' + String.toString.call(fn));
1048
+ return fn.bind(this);
1049
+ }
917
1050
  /** Handler */
918
1051
  handler(req, res, next, method) {
919
1052
  const nextAfter = next;
@@ -1013,7 +1146,7 @@ export class Router extends EventEmitter {
1013
1146
  url,
1014
1147
  middlewares
1015
1148
  });
1016
- middlewares = middlewares.map(i => this.server.bind(i));
1149
+ middlewares = middlewares.map(m => this.bind(m));
1017
1150
  let item = this._tree[method];
1018
1151
  if (!item)
1019
1152
  item = this._tree[method] = { tree: {} };
@@ -1049,7 +1182,6 @@ export class Router extends EventEmitter {
1049
1182
  if (!item[key])
1050
1183
  item[key] = [];
1051
1184
  item[key].push(...middlewares);
1052
- return this;
1053
1185
  }
1054
1186
  /** Clear routes and middlewares */
1055
1187
  clear() {
@@ -1063,71 +1195,79 @@ export class Router extends EventEmitter {
1063
1195
  *
1064
1196
  * @signature add(plugin: Plugin)
1065
1197
  * @param {Plugin} plugin plugin module instance
1066
- * @return {Router} current router
1198
+ * @return {Promise<>}
1067
1199
  *
1068
1200
  * @signature add(pluginid: string, ...args: any)
1069
1201
  * @param {string} pluginid pluginid module
1070
1202
  * @param {...any} args arguments passed to constructor
1071
- * @return {Router} current router
1203
+ * @return {Promise<>}
1072
1204
  *
1073
1205
  * @signature add(pluginClass: typeof Plugin, ...args: any)
1074
1206
  * @param {typeof Plugin} pluginClass plugin class
1075
1207
  * @param {...any} args arguments passed to constructor
1076
- * @return {Router} current router
1208
+ * @return {Promise<>}
1077
1209
  *
1078
1210
  * @signature add(middleware: Middleware)
1079
1211
  * @param {Middleware} middleware
1080
- * @return {Router} current router
1212
+ * @return {Promise<>}
1081
1213
  *
1082
1214
  * @signature add(methodUrl: string, ...middlewares: any)
1083
1215
  * @param {string} methodUrl 'METHOD /url' or '/url'
1084
1216
  * @param {...any} middlewares
1085
- * @return {Router} current router
1217
+ * @return {Promise<>}
1086
1218
  *
1087
1219
  * @signature add(methodUrl: string, controllerClass: typeof Controller)
1088
1220
  * @param {string} methodUrl 'METHOD /url' or '/url'
1089
1221
  * @param {typeof Controller} controllerClass
1090
- * @return {Router} current router
1222
+ * @return {Promise<>}
1091
1223
  *
1092
1224
  * @signature add(methodUrl: string, routes: Array<Array<any>>)
1093
1225
  * @param {string} methodUrl 'METHOD /url' or '/url'
1094
1226
  * @param {Array<Array<any>>} routes list with subroutes: ['METHOD /suburl', ...middlewares]
1095
- * @return {Router} current router
1227
+ * @return {Promise<>}
1096
1228
  *
1097
1229
  * @signature add(methodUrl: string, routes: Array<Array<any>>)
1098
1230
  * @param {string} methodUrl 'METHOD /url' or '/url'
1099
1231
  * @param {Array<Array<any>>} routes list with subroutes: ['METHOD /suburl', ...middlewares]
1100
- * @return {Router} current router
1232
+ * @return {Promise<>}
1101
1233
  *
1102
1234
  * @signature add(routes: { [key: string]: Array<any> })
1103
1235
  * @param { {[key: string]: Array<any>} } routes list with subroutes: 'METHOD /suburl': [...middlewares]
1104
- * @return {Router} current router
1236
+ * @return {Promise<>}
1105
1237
  *
1106
1238
  * @signature add(methodUrl: string, routes: { [key: string]: Array<any> })
1107
1239
  * @param {string} methodUrl 'METHOD /url' or '/url'
1108
1240
  * @param { {[key: string]: Array<any>} } routes list with subroutes: 'METHOD /suburl': [...middlewares]
1109
- * @return {Router} current router
1241
+ * @return {Promise<>}
1110
1242
  */
1111
- use(...args) {
1243
+ async use(...args) {
1112
1244
  if (!args[0])
1113
- return this;
1245
+ return;
1246
+ this.server._waiter.startJob();
1247
+ for (let i = 0; i < args.length; i++)
1248
+ args[i] = await args[i];
1114
1249
  // use(plugin)
1115
- if (args[0] instanceof Plugin)
1116
- return this._plugin(args[0]);
1250
+ if (args[0] instanceof Plugin) {
1251
+ await this._plugin(args[0]);
1252
+ return this.server._waiter.endJob();
1253
+ }
1117
1254
  // use(pluginid, ...args)
1118
1255
  if (typeof args[0] === 'string' && MicroServer.plugins[args[0]]) {
1119
1256
  const constructor = MicroServer.plugins[args[0]];
1120
1257
  const plugin = new constructor(this, ...args.slice(1));
1121
- return this._plugin(plugin);
1258
+ await this._plugin(plugin);
1259
+ return this.server._waiter.endJob();
1122
1260
  }
1123
1261
  // use(PluginClass, ...args)
1124
- if (args[0].prototype instanceof Plugin) {
1262
+ if (typeof args[0] === 'function' && args[0].prototype instanceof Plugin) {
1125
1263
  const plugin = new args[0](this, ...args.slice(1));
1126
- return this._plugin(plugin);
1264
+ await this._plugin(plugin);
1265
+ return this.server._waiter.endJob();
1127
1266
  }
1128
1267
  // use(middleware)
1129
- if (typeof args[0] === 'function') {
1130
- return this._middleware(args[0]);
1268
+ if (isFunction(args[0])) {
1269
+ this._middleware(args[0]);
1270
+ return this.server._waiter.endJob();
1131
1271
  }
1132
1272
  let method = '*', url = '/';
1133
1273
  if (typeof args[0] === 'string') {
@@ -1142,7 +1282,7 @@ export class Router extends EventEmitter {
1142
1282
  }
1143
1283
  // use('/url', ControllerClass)
1144
1284
  if (typeof args[0] === 'function' && args[0].prototype instanceof Controller) {
1145
- const routes = args[0].routes();
1285
+ const routes = await args[0].routes();
1146
1286
  if (routes)
1147
1287
  args[0] = routes;
1148
1288
  }
@@ -1150,17 +1290,17 @@ export class Router extends EventEmitter {
1150
1290
  if (Array.isArray(args[0])) {
1151
1291
  if (method !== '*')
1152
1292
  throw new Error('Invalid router usage');
1153
- args[0].forEach(item => {
1293
+ for (const item of args[0]) {
1154
1294
  if (Array.isArray(item)) {
1155
1295
  // [methodUrl, ...middlewares]
1156
1296
  if (typeof item[0] !== 'string' || !item[0].match(/^(\w+ )?\//))
1157
1297
  throw new Error('Url expected');
1158
- return this.use(item[0].replace(/\//, (url === '/' ? '' : url) + '/'), ...item.slice(1));
1298
+ await this.use(item[0].replace(/\//, (url === '/' ? '' : url) + '/'), ...item.slice(1));
1159
1299
  }
1160
1300
  else
1161
1301
  throw new Error('Invalid param');
1162
- });
1163
- return this;
1302
+ }
1303
+ return this.server._waiter.endJob();
1164
1304
  }
1165
1305
  // use('/url', {'METHOD /url': [...middlewares], ... } ])
1166
1306
  if (typeof args[0] === 'object' && args[0].constructor === Object) {
@@ -1169,42 +1309,47 @@ export class Router extends EventEmitter {
1169
1309
  for (const [subUrl, subArgs] of Object.entries(args[0])) {
1170
1310
  if (!subUrl.match(/^(\w+ )?\//))
1171
1311
  throw new Error('Url expected');
1172
- this.use(subUrl.replace(/\//, (url === '/' ? '' : url) + '/'), ...(Array.isArray(subArgs) ? subArgs : [subArgs]));
1312
+ await this.use(subUrl.replace(/\//, (url === '/' ? '' : url) + '/'), ...(Array.isArray(subArgs) ? subArgs : [subArgs]));
1173
1313
  }
1174
- return this;
1314
+ return this.server._waiter.endJob();
1175
1315
  }
1176
1316
  // use('/url', ...middleware)
1177
- return this._add(method, url, 'next', args.filter((o) => o));
1317
+ this._add(method, url, 'next', args.filter((o) => o));
1318
+ return this.server._waiter.endJob();
1178
1319
  }
1179
1320
  _middleware(middleware) {
1180
1321
  if (!middleware)
1181
- return this;
1322
+ return;
1182
1323
  const priority = (middleware?.priority || 0) - 1;
1183
1324
  const stack = priority < -1 ? this._stackAfter : this._stack;
1184
1325
  const idx = stack.findIndex(f => 'priority' in f
1185
1326
  && priority >= (f.priority || 0));
1186
1327
  stack.splice(idx < 0 ? stack.length : idx, 0, middleware);
1187
- return this;
1188
1328
  }
1189
- _plugin(plugin) {
1329
+ async _plugin(plugin) {
1330
+ let added;
1190
1331
  if (plugin.name) {
1191
1332
  if (this.plugins[plugin.name])
1192
1333
  throw new Error(`Plugin ${plugin.name} already added`);
1193
1334
  this.plugins[plugin.name] = plugin;
1335
+ added = plugin.name;
1194
1336
  }
1337
+ await plugin.initialise?.();
1195
1338
  if (plugin.handler) {
1196
1339
  const middleware = plugin.handler.bind(plugin);
1197
1340
  middleware.plugin = plugin;
1198
1341
  middleware.priority = plugin.priority;
1199
- return this._middleware(middleware);
1200
- }
1201
- if (plugin.routes) {
1202
- if (typeof plugin.routes === 'function')
1203
- this.use(plugin.routes());
1204
- else
1205
- this.use(plugin.routes);
1342
+ this._middleware(middleware);
1206
1343
  }
1207
- return this;
1344
+ if (plugin.routes)
1345
+ await this.use(isFunction(plugin.routes) ? await plugin.routes() : plugin.routes);
1346
+ if (added)
1347
+ this._waiter.resolve(added);
1348
+ }
1349
+ async waitPlugin(id) {
1350
+ if (!this.plugins[id])
1351
+ await this._waiter.wait(id);
1352
+ return this.plugins[id];
1208
1353
  }
1209
1354
  /** Add hook */
1210
1355
  hook(url, ...mid) {
@@ -1212,7 +1357,7 @@ export class Router extends EventEmitter {
1212
1357
  let method = '*';
1213
1358
  if (m)
1214
1359
  [method, url] = [m[1], m[2]];
1215
- return this._add(method, url, 'hook', mid);
1360
+ this._add(method, url, 'hook', mid);
1216
1361
  }
1217
1362
  /** Check if middleware allready added */
1218
1363
  has(mid) {
@@ -1220,10 +1365,9 @@ export class Router extends EventEmitter {
1220
1365
  }
1221
1366
  }
1222
1367
  export class MicroServer extends EventEmitter {
1223
- get plugins() { return this.router.plugins; }
1224
1368
  constructor(config) {
1225
1369
  super();
1226
- this._ready = false;
1370
+ this._waiter = new EmitterWaiter(this);
1227
1371
  this._methods = {};
1228
1372
  let promise = Promise.resolve();
1229
1373
  this._init = (f, ...args) => {
@@ -1255,7 +1399,7 @@ export class MicroServer extends EventEmitter {
1255
1399
  }
1256
1400
  /** Add one time listener or call immediatelly for 'ready' */
1257
1401
  once(name, cb) {
1258
- if (name === 'ready' && this._ready)
1402
+ if (name === 'ready' && this.isReady())
1259
1403
  cb();
1260
1404
  else
1261
1405
  super.once(name, cb);
@@ -1263,11 +1407,22 @@ export class MicroServer extends EventEmitter {
1263
1407
  }
1264
1408
  /** Add listener and call immediatelly for 'ready' */
1265
1409
  on(name, cb) {
1266
- if (name === 'ready' && this._ready)
1410
+ if (name === 'ready' && this.isReady())
1267
1411
  cb();
1268
1412
  super.on(name, cb);
1269
1413
  return this;
1270
1414
  }
1415
+ isReady() {
1416
+ return !this._waiter.busy;
1417
+ }
1418
+ async waitReady() {
1419
+ if (this.isReady())
1420
+ return;
1421
+ return this._waiter.wait("ready");
1422
+ }
1423
+ async waitPlugin(id) {
1424
+ await this.router.waitPlugin(id);
1425
+ }
1271
1426
  /** Listen server, should be used only if config.listen is not set */
1272
1427
  listen(config) {
1273
1428
  const listen = (config?.listen || this.config.listen || 0) + '';
@@ -1293,164 +1448,62 @@ export class MicroServer extends EventEmitter {
1293
1448
  });
1294
1449
  }
1295
1450
  }
1296
- return new Promise((resolve) => {
1297
- let readyCount = 0;
1298
- this._ready = false;
1299
- const ready = (srv) => {
1300
- if (srv)
1301
- readyCount++;
1302
- if (readyCount >= this.servers.size) {
1303
- if (!this._ready) {
1304
- this._ready = true;
1305
- if (this.servers.size === 0)
1306
- this.close();
1307
- else
1308
- this.emit('ready');
1309
- resolve();
1310
- }
1311
- }
1312
- };
1313
- const reg = /^((?<proto>\w+):\/\/)?(?<host>(\[[^\]]+\]|[a-z][^:,]+|\d+\.\d+\.\d+\.\d+))?:?(?<port>\d+)?/;
1314
- listen.split(',').forEach(listen => {
1315
- let { proto, host, port } = reg.exec(listen)?.groups || {};
1316
- let srv;
1317
- switch (proto) {
1318
- case 'tcp':
1319
- if (!config?.handler)
1320
- throw new Error('Handler is required for tcp');
1321
- srv = net.createServer(handler);
1322
- break;
1323
- case 'tls':
1324
- if (!config?.handler)
1325
- throw new Error('Handler is required for tls');
1326
- srv = tls.createServer(tlsOptions(), handler);
1327
- tlsOptionsReload(srv);
1328
- break;
1329
- case 'https':
1330
- port = port || '443';
1331
- srv = https.createServer(tlsOptions(), handler);
1332
- tlsOptionsReload(srv);
1333
- break;
1334
- default:
1335
- port = port || '80';
1336
- srv = http.createServer(handler);
1337
- break;
1338
- }
1339
- this.servers.add(srv);
1340
- if (port === '0') // skip listening
1341
- ready(srv);
1342
- else {
1343
- srv.listen(parseInt(port), host?.replace(/[\[\]]/g, '') || '0.0.0.0', () => {
1344
- const addr = srv.address();
1345
- this.emit('listen', addr.port, addr.address, srv);
1346
- ready(srv);
1347
- });
1348
- }
1349
- srv.on('error', err => {
1350
- this.servers.delete(srv);
1351
- srv.close();
1352
- ready();
1353
- this.emit('error', err);
1354
- });
1355
- srv.on('connection', s => {
1356
- this.sockets.add(s);
1357
- s.once('close', () => this.sockets.delete(s));
1451
+ const reg = /^((?<proto>\w+):\/\/)?(?<host>(\[[^\]]+\]|[a-z][^:,]+|\d+\.\d+\.\d+\.\d+))?:?(?<port>\d+)?/;
1452
+ listen.split(',').forEach(listen => {
1453
+ this._waiter.startJob();
1454
+ let { proto, host, port } = reg.exec(listen)?.groups || {};
1455
+ let srv;
1456
+ switch (proto) {
1457
+ case 'tcp':
1458
+ if (!config?.handler)
1459
+ throw new Error('Handler is required for tcp');
1460
+ srv = net.createServer(handler);
1461
+ break;
1462
+ case 'tls':
1463
+ if (!config?.handler)
1464
+ throw new Error('Handler is required for tls');
1465
+ srv = tls.createServer(tlsOptions(), handler);
1466
+ tlsOptionsReload(srv);
1467
+ break;
1468
+ case 'https':
1469
+ port = port || '443';
1470
+ srv = https.createServer(tlsOptions(), handler);
1471
+ tlsOptionsReload(srv);
1472
+ break;
1473
+ default:
1474
+ port = port || '80';
1475
+ srv = http.createServer(handler);
1476
+ break;
1477
+ }
1478
+ this.servers.add(srv);
1479
+ if (port === '0') // skip listening
1480
+ this._waiter.endJob();
1481
+ else {
1482
+ srv.listen(parseInt(port), host?.replace(/[\[\]]/g, '') || '0.0.0.0', () => {
1483
+ const addr = srv.address();
1484
+ this.emit('listen', addr.port, addr.address, srv);
1485
+ srv._ready = true;
1486
+ this._waiter.endJob();
1358
1487
  });
1359
- srv.on('upgrade', this.handlerUpgrade.bind(this));
1360
- ready();
1488
+ }
1489
+ srv.on('error', err => {
1490
+ srv.close();
1491
+ this.servers.delete(srv);
1492
+ if (!srv._ready)
1493
+ this._waiter.endJob();
1494
+ this.emit('error', err);
1495
+ });
1496
+ srv.on('connection', s => {
1497
+ this.sockets.add(s);
1498
+ s.once('close', () => this.sockets.delete(s));
1361
1499
  });
1500
+ srv.on('upgrade', this.handlerUpgrade.bind(this));
1362
1501
  });
1363
- }
1364
- /** bind middleware or create one from string like: 'redirect:302,https://redirect.to', 'error:422', 'param:name=value', 'acl:users/get', 'model:User', 'group:Users', 'user:admin' */
1365
- bind(fn) {
1366
- if (typeof fn === 'string') {
1367
- let name = fn;
1368
- let idx = name.indexOf(':');
1369
- if (idx < 0 && name.includes('=')) {
1370
- name = 'param:' + name;
1371
- idx = 5;
1372
- }
1373
- if (idx >= 0) {
1374
- const v = name.slice(idx + 1);
1375
- const type = name.slice(0, idx);
1376
- // predefined middlewares
1377
- switch (type) {
1378
- // redirect:302,https://redirect.to
1379
- case 'redirect': {
1380
- let redirect = v.split(','), code = parseInt(v[0]);
1381
- if (!code || code < 301 || code > 399)
1382
- code = 302;
1383
- return (req, res) => res.redirect(code, redirect[1] || v);
1384
- }
1385
- // error:422
1386
- case 'error':
1387
- return (req, res) => res.error(parseInt(v) || 422);
1388
- // param:name=value
1389
- case 'param': {
1390
- idx = v.indexOf('=');
1391
- if (idx > 0) {
1392
- const prm = v.slice(0, idx), val = v.slice(idx + 1);
1393
- return (req, res, next) => { req.params[prm] = val; return next(); };
1394
- }
1395
- break;
1396
- }
1397
- case 'model': {
1398
- const model = v;
1399
- return (req, res) => {
1400
- res.isJson = true;
1401
- req.params.model = model;
1402
- req.model = Model.models[model];
1403
- if (!req.model) {
1404
- console.error(`Data model ${model} not defined for request ${req.path}`);
1405
- return res.error(422);
1406
- }
1407
- return req.model.handler(req, res);
1408
- };
1409
- }
1410
- // user:userid
1411
- // group:user_groupid
1412
- // acl:validacl
1413
- case 'user':
1414
- case 'group':
1415
- case 'acl':
1416
- return (req, res, next) => {
1417
- if (type === 'user' && v === req.user?.id)
1418
- return next();
1419
- if (type === 'acl') {
1420
- req.params.acl = v;
1421
- if (req.auth?.acl(v))
1422
- return next();
1423
- }
1424
- if (type === 'group') {
1425
- req.params.group = v;
1426
- if (req.user?.group === v)
1427
- return next();
1428
- }
1429
- const accept = req.headers.accept || '';
1430
- if (!res.isJson && req.auth?.options.redirect && req.method === 'GET' && !accept.includes('json') && (accept.includes('html') || accept.includes('*/*'))) {
1431
- if (req.auth.options.redirect && req.url !== req.auth.options.redirect)
1432
- return res.redirect(302, req.auth.options.redirect);
1433
- else if (req.auth.options.mode !== 'cookie') {
1434
- res.setHeader('WWW-Authenticate', `Basic realm="${req.auth.options.realm}"`);
1435
- return res.error(401);
1436
- }
1437
- }
1438
- return res.error('Permission denied');
1439
- };
1440
- }
1441
- }
1442
- throw new Error('Invalid option: ' + name);
1443
- }
1444
- if (fn && typeof fn === 'object' && 'handler' in fn && typeof fn.handler === 'function')
1445
- return fn.handler.bind(fn);
1446
- if (typeof fn !== 'function')
1447
- throw new Error('Invalid middleware: ' + String.toString.call(fn));
1448
- return fn.bind(this);
1502
+ return this._waiter.wait('ready');
1449
1503
  }
1450
1504
  /** Add middleware, routes, etc.. see {router.use} */
1451
1505
  use(...args) {
1452
- this.router.use(...args);
1453
- return this;
1506
+ return this.router.use(...args);
1454
1507
  }
1455
1508
  /** Default server handler */
1456
1509
  handler(req, res) {
@@ -1592,8 +1645,8 @@ export class MicroServer extends EventEmitter {
1592
1645
  }
1593
1646
  this.sockets.clear();
1594
1647
  }).then(() => {
1595
- this._ready = false;
1596
1648
  this.emit('close');
1649
+ this._waiter.resolve("close");
1597
1650
  });
1598
1651
  }
1599
1652
  /** Add route, alias to `server.router.use(url, ...args)` */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radatek/microserver",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "HTTP MicroServer",
5
5
  "author": "Darius Kisonas",
6
6
  "license": "MIT",
@@ -11,12 +11,9 @@
11
11
  "httpserver"
12
12
  ],
13
13
  "type": "module",
14
- "main": "./dist/microserver.js",
15
- "module": "./dist/microserver.js",
16
- "types": "./dist/microserver.d.ts",
17
- "files": [
18
- "dist"
19
- ],
14
+ "main": "microserver.js",
15
+ "module": "microserver.js",
16
+ "types": "microserver.d.ts",
20
17
  "repository": {
21
18
  "type": "git",
22
19
  "url": "git+https://github.com/radateklt/microserver.git"
package/readme.md DELETED
@@ -1,139 +0,0 @@
1
- ## HTTP MicroServer
2
-
3
- Lightweight all-in-one http web server without dependencies
4
-
5
- Features:
6
- - fast REST API router
7
- - form/json body decoder
8
- - file upload
9
- - websockets
10
- - authentication
11
- - plain/hashed passwords
12
- - virtual hosts
13
- - static files
14
- - rewrite
15
- - redirect
16
- - reverse proxy routes
17
- - trust ip for reverse proxy
18
- - json file storage with autosave
19
- - tls with automatic certificate reload
20
- - data model with validation and mongodb interface
21
- - simple file/memory storage
22
- - promises as middleware
23
- - controller class
24
- - access rights per route
25
- - access rights per model field
26
-
27
- ### Usage examples:
28
-
29
- Simple router:
30
-
31
- ```ts
32
- import { MicroServer, AccessDenied } from '@radatek/microserver'
33
-
34
- const server = new MicroServer({
35
- listen: 8080,
36
- auth: {
37
- users: {
38
- usr: {
39
- password: 'secret'
40
- }
41
- }
42
- }
43
- })
44
- server.use('GET /api/hello/:id',
45
- (req: ServerRequset, res: ServerResponse) =>
46
- ({message:'Hello ' + req.params.id + '!'}))
47
- server.use('POST /api/login',
48
- (req: ServerRequset, res: ServerResponse) =>
49
- {
50
- const user = await req.auth.login(req.body.user, req.body.password)
51
- return user ? {user} : new AccessDenied()
52
- })
53
- server.use('GET /api/protected', 'acl:auth',
54
- (req: ServerRequset, res: ServerResponse) =>
55
- ({message:'Secret resource'}))
56
- server.use('static', {root:'public'})
57
- ```
58
-
59
- Using data schema:
60
-
61
- ```js
62
- import { MicroServer, Model, MicroCollection, FileStore } from '@radatek/microserver'
63
-
64
- const usersCollection = new MicroCollection({ store: new FileStore({ dir: 'data' }), name: 'users' })
65
- // or using MicroDB collection
66
- const usersCollection = await db.collection('users')
67
-
68
- const userProfile = new Model({
69
- _id: 'string',
70
- name: { type: 'string', required: true },
71
- email: { type: 'string', format: 'email' },
72
- password: { type: 'string', canRead: false },
73
- role: { type: 'string' },
74
- acl: { type: 'object' },
75
- }, { collection: usersCollection, name: 'user' })
76
-
77
- const server = new MicroServer({
78
- listen:8080,
79
- auth: {
80
- users: (user, password) => userProfile.get(password ? {_id: user, password } : {_id: user})
81
- }
82
- })
83
-
84
- await userProfile.insert({name: 'admin', password: 'secret', role: 'admin', acl: {'user/*': true}})
85
-
86
- server.use('POST /login', async (req) => {
87
- const user = await req.auth.login(req.body.user, req.body.password)
88
- return user ? { user } : 403
89
- })
90
- // authenticated user allways has auth access
91
- server.use('GET /profile', 'acl:auth', req => ({ user: req.user }))
92
- // get all users if role='admin'
93
- server.use('GET /admin/users', 'role:admin', userProfile)
94
- // get user by id if has acl 'user/get'
95
- server.use('GET /admin/user/:id', 'acl:user/get', userProfile)
96
- // insert new user if role='admin' and has acl 'user/insert'
97
- server.use('POST /admin/user', 'role:admin', 'acl:user/insert', userProfile)
98
- // update user if has acl 'user/update'
99
- server.use('PUT /admin/user/:id', 'acl:user/update', userProfile)
100
- // delete user if has acl 'user/update'
101
- server.use('DELETE /admin/user/:id', 'acl:user/delete', userProfile)
102
- ```
103
-
104
- Using controller:
105
-
106
- ```ts
107
- const server = new MicroServer({
108
- listen: 8080,
109
- auth: {
110
- users: {
111
- usr: {
112
- password: 'secret',
113
- acl: {user: true}
114
- }
115
- }
116
- }
117
- })
118
-
119
- class RestApi extends Controller {
120
- static acl = '' // default acl
121
-
122
- gethello(id) {
123
- return {message:'Hello ' + id + '!'}
124
- }
125
-
126
- async postlogin() {
127
- const user = await this.auth.login(this.body.user, this.body.password)
128
- return user ? {user} : 403
129
- }
130
-
131
- static 'acl:protected' = 'user'
132
- static 'url:protected' = 'GET /protected'
133
- protected() {
134
- return {message:'Protected'}
135
- }
136
- }
137
-
138
- server.use('/api', RestApi)
139
- ```