@powership/server
Version:
280 lines (275 loc) • 8.4 kB
JavaScript
import * as HTTP from 'http';
import { hope, NodeLogger } from '@powership/utils';
import { nonNullValues } from '@powership/utils';
import httpErrors from 'http-errors';
import { createAsyncPlugin } from 'plugin-hooks';
import { isHttpError } from "./BaseRequestHandler.mjs";
import { ServerLogs } from "./ServerLogs.mjs";
import { ServerRequest } from "./ServerRequest.mjs";
import { ServerResponse } from "./ServerResponse.mjs";
import { _404 } from "./_404.mjs";
import { parseHTTPBody } from "./bodyParserHandler.mjs";
import { createHandler } from "./createHandler.mjs";
const {
InternalServerError,
NotFound
} = httpErrors;
export class Server {
defaultHandlers = [createHandler('defaultErrorHandler', {
// Handling errors
onError(_response, {
close,
error
}) {
if (this.handledCount) return;
// initial Response
ServerLogs.error(error);
const httpError = isHttpError(error) ? error : new InternalServerError();
const errorResponse = ServerResponse.create(httpError);
close(errorResponse);
}
}), createHandler('notFoundHandler', {
onResponse(_request, {
close
}) {
if (this.handledCount) return; // TODO should be the last handler. check
const httpError = new NotFound();
httpError.body = _404();
const errorResponse = ServerResponse.create(httpError);
close(errorResponse);
}
})];
constructor(definition) {
this.definitionInput = definition;
this.handlers = [...definition.handlers, ...this.defaultHandlers];
}
static create = definition => {
return new Server(definition);
};
hooks = createHooks();
closeServer = async () => {
const server = this.server;
if (!server) return 'NOT_STARTED';
return new Promise((resolve, reject) => {
server.close(err => {
if (err) return reject(err);
resolve('CLOSED');
});
});
};
hasStarted = false;
handleRequest = request => {
let closed = false;
const app = this;
const hopeForResponse = hope();
(async () => {
try {
if (!app.hasStarted) {
await this.start();
}
const hooks = app.hooks;
await (async () => {
const close = function close(...args) {
//
const finalResponse = (() => {
const arg0 = args[0];
if (!arg0) return appResponse;
if (typeof arg0 === 'object') return arg0;
appResponse.statusCode = arg0;
return appResponse;
})();
if (closed || hopeForResponse.result || hopeForResponse.error) {
closed = true;
return;
} else {
closed = true;
}
if (!finalResponse) {
closed = true;
hopeForResponse.resolve(appResponse);
return;
}
try {
hopeForResponse.resolve(finalResponse);
} catch (e) {
if (!(hopeForResponse.result || hopeForResponse.error)) {
onError(e).catch(ServerLogs.error);
}
}
};
let appResponse = ServerResponse.create();
request.body = await hooks.onParseBody.dispatch(request.body, {
req: request,
res: appResponse,
app,
close
});
request = await hooks.onRequest.dispatch(request, {
app,
response: appResponse,
close
});
appResponse = await hooks.onResponse.dispatch(appResponse, {
app,
request,
close
});
async function onError(error) {
const errorResponse = await hooks.onError.dispatch(appResponse, {
request,
error,
close
});
close(errorResponse);
}
if (!closed) {
close(appResponse);
}
})();
} catch (e) {
hopeForResponse.reject(e);
}
})();
return hopeForResponse;
};
withServer = false;
async start(port) {
const app = this;
if (!app.hasStarted) {
this._startHandlers();
}
if (app.hasStarted) {
ServerLogs.info('RESTARTING_APP');
await this.closeServer();
}
app.hasStarted = true;
this.usedDefinition = await (() => {
if (typeof this.definitionInput === 'function') {
return this.definitionInput(this);
}
return this.definitionInput;
})();
const hooks = this.hooks;
if (port === undefined) return app;
if (!hooks.onResponse.middlewares.length) {
NodeLogger.logWarning(`⚠️ No handlers listening to onResponse.`);
}
let server = HTTP.createServer(async function _requestListener(httpServerRequest, httpServerResponse) {
try {
const {
httpMethod
} = nonNullValues({
httpMethod: httpServerRequest.method
});
const requestBody = await parseHTTPBody(httpServerRequest, httpServerResponse);
const appRequest = ServerRequest.create({
body: requestBody,
headers: httpServerRequest.headers,
locals: {},
url: httpServerRequest.url,
method: httpMethod
});
const response = await app.handleRequest(appRequest);
const {
statusCode,
body,
headersNamed,
streamBody
} = response.toHttpResponse();
httpServerResponse.statusCode = statusCode;
headersNamed.forEach(({
name,
value
}) => {
httpServerResponse.setHeader(name, value);
});
if (streamBody) {
streamBody.on('error', error => {
ServerLogs.error(error);
httpServerResponse.statusCode = 500;
httpServerResponse.end();
});
streamBody.pipe(httpServerResponse);
} else {
httpServerResponse.end(body);
}
} catch (error) {
console.trace(error);
ServerLogs.error(error);
if (httpServerResponse.headersSent) {
ServerLogs.error(`ERROR_AFTER_HEADERS_SENT`, error);
} else {
const error = new InternalServerError();
httpServerResponse.statusCode = error.statusCode;
if (error.headers) {
Object.entries(error.headers).forEach(([k, v]) => {
httpServerResponse.setHeader(k, v);
});
}
httpServerResponse.end(error.message);
}
}
});
// server = await this.hooks.willStartServer.dispatch(server, { app: this });
return new Promise(async (resolve, reject) => {
try {
server.listen(port, () => {
const addressInfo = server.address();
if (!addressInfo || typeof addressInfo !== 'object') {
ServerLogs.info({
addressInfo
});
throw new Error(`Failed to recovery server addressInfo.`);
}
app.withServer = {
server,
...addressInfo
};
resolve(Object.assign(app, {
server,
...addressInfo
}));
});
await this.hooks.started.dispatch(server, {
app: this
});
} catch (e) {
reject(e);
}
});
}
_startHandlers = () => {
const handlerByName = {};
this.handlers.forEach(appHandler => {
const {
hooks,
name
} = appHandler;
if (handlerByName[name] && handlerByName[name] !== appHandler) {
console.warn(`Handler with name "${name}" already registered.`);
} else {
handlerByName[name] = appHandler;
}
Object.entries(hooks).forEach(([hookName, handler]) => {
try {
Object.defineProperty(handler, 'name', {
value: `${name}_${hookName}`
});
} catch (e) {}
this.hooks[hookName].pushMiddleware(handler);
});
});
return handlerByName;
};
}
function createHooks() {
return {
willStartServer: createAsyncPlugin(),
started: createAsyncPlugin(),
onParseBody: createAsyncPlugin(),
onRequest: createAsyncPlugin(),
onResponse: createAsyncPlugin(),
onError: createAsyncPlugin()
};
}
//# sourceMappingURL=Server.mjs.map