UNPKG

@ima/server

Version:

Default dev server for IMA.js applications.

379 lines (311 loc) 9.65 kB
const { RouteNames } = require('@ima/core'); const { Event } = require('../emitter.js'); module.exports = function hooksFactory({ renderOverloadedPage, renderStaticSPAPage, renderStaticServerErrorPage, renderStaticClientErrorPage, urlParser, _initApp, _clearApp, _importAppMainSync, _addImaToResponse, _getRouteInfo, _generateAppResponse, processContent, createContentVariables, sendResponseHeaders, emitter, instanceRecycler, devErrorPage, environment, }) { function _isServerOverloaded(event) { const { environment } = event; if (environment.$Server.degradation) { return environment.$Server.degradation?.isOverloaded?.(event) ?? false; } return ( environment.$Server.overloadConcurrency !== undefined && instanceRecycler.getConcurrentRequests() + 1 > environment.$Server.overloadConcurrency ); } function _isValidResponse(event) { const { res, context } = event; const isRedirectResponse = context.response.status >= 300 && context.response.status < 400 && context.response.url; if (res.headersSent || isRedirectResponse || !context.response) { return false; } return true; } function _hasToServeSPA(event) { if (process.env.IMA_CLI_FORCE_SPA) { return true; } const { req, environment } = event; const spaConfig = environment.$Server.serveSPA; const isAllowedServeSPA = spaConfig.allow; let isServerBusy = instanceRecycler.hasReachedMaxConcurrentRequests(); if (environment.$Server.degradation) { isServerBusy = environment.$Server.degradation?.isSPA?.(event) ?? false; return isAllowedServeSPA && isServerBusy; } const userAgent = req.headers['user-agent'] || ''; const isAllowedUserAgent = !( spaConfig.blackList && typeof spaConfig.blackList === 'function' && spaConfig.blackList(userAgent) ); return isAllowedServeSPA && isServerBusy && isAllowedUserAgent; } function _hasToLoadApp(event) { const { environment } = event; return !( (environment.$Server.serveSPA?.allow && environment.$Server.concurrency === 0) || process.env.IMA_CLI_FORCE_SPA ); } function _hasToServeStatic(event) { const { environment } = event; if (environment.$Server.degradation) { return environment.$Server.degradation?.isStatic?.(event) ?? false; } return ( environment.$Server.staticConcurrency !== undefined && instanceRecycler.getConcurrentRequests() + 1 > environment.$Server.staticConcurrency ); } function _hasToServeStaticBadRequest(event) { const { req, res } = event; const routeInfo = _getRouteInfo({ req, res }); const isBadRequest = routeInfo && routeInfo.route.getName() === RouteNames.NOT_FOUND; // TODO IMA@19 documentation badRequestConcurrency return isBadRequest && _hasToServeStatic(event); } function _hasToServeStaticServerError(event) { const { req, res } = event; const routeInfo = _getRouteInfo({ req, res }); const isServerError = routeInfo && routeInfo.route.getName() === RouteNames.ERROR; return isServerError && _hasToServeStatic(event); } async function _applyError(event) { if (!event.context?.app || _hasToServeStatic(event)) { return renderStaticServerErrorPage(event); } try { const { error, context } = event; return context.app.oc .get('$Router') .handleError({ error }) .catch(e => { e.cause = error; return renderStaticServerErrorPage({ ...event, error: e }); }); } catch (e) { e.cause = event.error; return renderStaticServerErrorPage({ ...event, error: e }); } } async function _applyNotFound(event) { if (!event.context?.app || _hasToServeStatic(event)) { return renderStaticClientErrorPage(event); } try { const { error, context } = event; const router = context.app.oc.get('$Router'); return router.handleNotFound({ error }).catch(e => { e.cause = error; if (router.isRedirection(e)) { return _applyRedirect({ ...event, error: e }); } return _applyError({ ...event, error: e }); }); } catch (e) { e.cause = event.error; return _applyError({ ...event, error: e }); } } async function _applyRedirect(event) { try { const { error } = event; return { content: null, status: error.getHttpStatus(), error, url: error.getParams().url, }; } catch (e) { e.cause = event.error; return _applyError({ ...event, error: e }); } } async function renderError(event = {}) { if ( environment.$Debug && process.env.IMA_CLI_WATCH && !event.error.isRedirection?.() ) { return devErrorPage(event); } else { try { const { context, error } = event; if (context?.app) { context.app.oc.get('$Cache').clear(); } if (error.isClientError?.()) { return _applyNotFound(event); } else if (error.isRedirection?.()) { return _applyRedirect(event); } else { return _applyError(event); } } catch (e) { e.cause = event.error; return renderStaticServerErrorPage({ ...event, error: e }); } } } function userErrorHook() { emitter.on(Event.Error, async event => { return await renderError(event); }); } function useIMAInitializationRequestHook() { emitter.on(Event.Request, async event => { _hasToLoadApp(event) && _importAppMainSync(event); _addImaToResponse(event); }); } function userPerformanceOptimizationRequestHook() { emitter.on(Event.Request, async event => { if (_hasToServeSPA(event)) { event.stopPropagation(); return renderStaticSPAPage(event); } if (_isServerOverloaded(event)) { event.stopPropagation(); return renderOverloadedPage(event); } if (_hasToServeStaticBadRequest(event)) { event.stopPropagation(); return renderStaticClientErrorPage(event); } if (_hasToServeStaticServerError(event)) { event.stopPropagation(); return renderStaticServerErrorPage({ ...event, error: event.error ?? new Error('The App error route exceed static thresholds.'), }); } }); } function useIMAHandleRequestHook() { emitter.on(Event.Request, async event => { _initApp(event); event.stopPropagation(); return _generateAppResponse(event); }); } function useRequestHook() { useIMAInitializationRequestHook(); userPerformanceOptimizationRequestHook(); useIMAHandleRequestHook(); } function useBeforeRequestHook() { useUrlParserBeforeRequestHook(); } function useUrlParserBeforeRequestHook() { emitter.on(Event.BeforeRequest, async event => { urlParser(event); }); } function useCreateContentVariablesHook() { emitter.on(Event.CreateContentVariables, async event => { if (!_isValidResponse(event)) { return event.result; } return { ...event.result, ...createContentVariables(event), }; }); } function useResponseHook() { emitter.on(Event.BeforeResponse, async event => { if (!_isValidResponse(event)) { return; } const { context } = event; const isAppExists = context.app && typeof context.app !== 'function'; if (isAppExists) { const state = context.app.oc.get('$PageStateManager').getState(); const cache = context.app.oc.get('$Cache').serialize(); const { headers, cookie } = context.app.oc .get('$Response') .getResponseParams(); context.response.page = { ...context.response.page, ...{ state, cache, headers, cookie }, }; } // Store copy of BeforeResponse result before emitting new event const beforeResponseResult = { ...event.result }; // Generate content variables event = await emitter.emit(Event.CreateContentVariables, event); event.context.response.contentVariables = { ...event.result, }; // Restore before response event result contents event.result = beforeResponseResult; // Interpolate contentVariables into the response content event.context.response.content = processContent(event); }); emitter.on(Event.Response, async event => { const { res, context } = event; if (res.headersSent || !context.response) { return; } sendResponseHeaders({ res, context }); if ( context.response.status >= 300 && context.response.status < 400 && context.response.url ) { res.redirect(context.response.status, context.response.url); return; } res.status(context.response.status); res.send(context.response.content); }); emitter.on(Event.AfterResponse, async event => { _clearApp(event); }); } function useIMADefaultHook() { useCreateContentVariablesHook(); userErrorHook(); useBeforeRequestHook(); useRequestHook(); useResponseHook(); } return { useIMADefaultHook, userErrorHook, useBeforeRequestHook, useRequestHook, useResponseHook, useIMAHandleRequestHook, useIMAInitializationRequestHook, userPerformanceOptimizationRequestHook, }; };