Skip to content

Commit

Permalink
Merge pull request #116 from sima-land/38-examples-bun
Browse files Browse the repository at this point in the history
 Шаг 69 #38
  • Loading branch information
krutoo committed Mar 12, 2024
2 parents f4af172 + 995fcbb commit 3065a97
Show file tree
Hide file tree
Showing 13 changed files with 360 additions and 335 deletions.
2 changes: 1 addition & 1 deletion src/preset/bun-handler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
provideFetch,
provideReduxMiddlewareSaga,
} from '../isomorphic/providers';
import { providePageRender } from '../node-handler/providers';
import { providePageRender } from '../node-handler/providers/page-render';
import { SpecificExtras } from '../server/utils/specific-extras';
import { HandlerProviders } from './providers';

Expand Down
20 changes: 9 additions & 11 deletions src/preset/node-handler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,15 @@ import {
provideAbortController,
} from '../isomorphic/providers';
import { PresetTuner } from '../isomorphic/types';
import {
provideAxiosMiddleware,
provideAxiosLogHandler,
provideHandlerMain,
provideSpecificParams,
providePageHelmet,
providePageRender,
provideFetchMiddleware,
provideFetchLogHandler,
provideCookieStore,
} from './providers';
import { provideAxiosMiddleware } from './providers/axios-middleware';
import { provideAxiosLogHandler } from './providers/axios-log-handler';
import { provideHandlerMain } from './providers/handler-main';
import { provideSpecificParams } from './providers/specific-params';
import { providePageHelmet } from './providers/page-helmet';
import { providePageRender } from './providers/page-render';
import { provideFetchMiddleware } from './providers/fetch-middleware';
import { provideFetchLogHandler } from './providers/fetch-log-handler';
import { provideCookieStore } from './providers/cookie-store';
import { SpecificExtras } from '../server/utils/specific-extras';

/**
Expand Down
23 changes: 23 additions & 0 deletions src/preset/node-handler/providers/axios-log-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Resolve } from '../../../di';
import { KnownToken } from '../../../tokens';
import { LogMiddlewareHandlerInit } from '../../../utils/axios';
import { AxiosLogging } from '../../isomorphic/utils/axios-logging';

/**
* Провайдер обработчика логирования axios.
* @param resolve Функция для получения зависимости по токену.
* @return Обработчик логирования.
*/
export function provideAxiosLogHandler(resolve: Resolve): LogMiddlewareHandlerInit {
const logger = resolve(KnownToken.logger);
const abortController = resolve(KnownToken.Http.Fetch.abortController);

return data => {
const logHandler = new AxiosLogging(logger, data);

// ВАЖНО: отключаем логирование если запрос прерван
logHandler.disabled = () => abortController.signal.aborted;

return logHandler;
};
}
64 changes: 64 additions & 0 deletions src/preset/node-handler/providers/axios-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Middleware } from 'middleware-axios';
import { Resolve } from '../../../di';
import { KnownToken } from '../../../tokens';
import { HttpStatus } from '../../isomorphic/utils/http-status';
import { axiosTracingMiddleware } from '../../node/utils/axios-tracing-middleware';
import { cookieMiddleware, logMiddleware } from '../../../utils/axios';
import { getForwardedHeaders } from '../../node/utils/get-forwarded-headers';

/**
* Провайдер фабрики http-клиентов.
* @param resolve Функция для получения зависимости по токену.
* @return Фабрика.
*/
export function provideAxiosMiddleware(resolve: Resolve): Middleware<any>[] {
const appConfig = resolve(KnownToken.Config.base);
const tracer = resolve(KnownToken.Tracing.tracer);
const context = resolve(KnownToken.ExpressHandler.context);
const logHandler = resolve(KnownToken.Axios.Middleware.Log.handler);
const cookieStore = resolve(KnownToken.Http.Fetch.cookieStore);
const abortController = resolve(KnownToken.Http.Fetch.abortController);

return [
// пробрасываемые заголовки по соглашению
async (config, next) => {
await next({
...config,
headers: {
...getForwardedHeaders(appConfig, context.req),

// ВАЖНО: заголовки из конфига важнее, поэтому идут в конце
...config.headers,
},
});
},

// обрывание по сигналу из обработчика входящего запроса и по сигналу из конфига исходящего запроса
async (config, next) => {
const innerController = new AbortController();

abortController.signal.addEventListener(
'abort',
() => {
innerController.abort();
},
{ once: true },
);

config.signal?.addEventListener?.(
'abort',
() => {
innerController.abort();
},
{ once: true },
);

await next({ ...config, signal: innerController.signal });
},

HttpStatus.axiosMiddleware(),
axiosTracingMiddleware(tracer, context.res.locals.tracing.rootContext),
logMiddleware(logHandler),
cookieMiddleware(cookieStore),
];
}
29 changes: 29 additions & 0 deletions src/preset/node-handler/providers/cookie-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createCookieStore } from '../../../http';
import type { Resolve } from '../../../di';
import { KnownToken } from '../../../tokens';

/**
* Провайдер хранилища cookie для исходящих запросов.
* @param resolve Функция для получения зависимости по токену.
* @return Хранилище cookie.
*/
export function provideCookieStore(resolve: Resolve) {
const context = resolve(KnownToken.ExpressHandler.context);

const store = createCookieStore(context.req.header('cookie'));

// @todo
// store.subscribe(setCookieList => {
// for (const setCookie of setCookieList) {
// const parsed = parseSetCookieHeader(setCookie);

// if (!parsed) {
// return;
// }

// context.res.cookie(parsed.name, parsed.value, parsed.attrs);
// }
// });

return store;
}
21 changes: 21 additions & 0 deletions src/preset/node-handler/providers/fetch-log-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { LogHandler, LogHandlerFactory } from '../../../http';
import { Resolve } from '../../../di';
import { KnownToken } from '../../../tokens';
import { FetchLogging } from '../../isomorphic/utils/fetch-logging';

/**
* Провайдер обработчика логирования axios.
* @param resolve Функция для получения зависимости по токену.
* @return Обработчик логирования.
*/
export function provideFetchLogHandler(resolve: Resolve): LogHandler | LogHandlerFactory {
const logger = resolve(KnownToken.logger);
const abortController = resolve(KnownToken.Http.Fetch.abortController);

const logHandler = new FetchLogging(logger);

// ВАЖНО: отключаем логирование если запрос прерван
logHandler.disabled = () => abortController.signal.aborted;

return logHandler;
}
40 changes: 40 additions & 0 deletions src/preset/node-handler/providers/fetch-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Resolve } from '../../../di';
import { Middleware, cookie, defaultHeaders } from '../../../http';
import { KnownToken } from '../../../tokens';
import { getFetchErrorLogging } from '../../isomorphic/utils/get-fetch-error-logging';
import { getFetchExtraAborting } from '../../isomorphic/utils/get-fetch-extra-aborting';
import { getFetchLogging } from '../../isomorphic/utils/get-fetch-logging';
import { getFetchTracing } from '../../server/utils/get-fetch-tracing';
import { getForwardedHeaders } from '../../node/utils/get-forwarded-headers';

/**
* Провайдер промежуточных слоев для fetch.
* @param resolve Функция для получения зависимости по токену.
* @return Массив промежуточных слоев.
*/
export function provideFetchMiddleware(resolve: Resolve): Middleware[] {
const config = resolve(KnownToken.Config.base);
const tracer = resolve(KnownToken.Tracing.tracer);
const context = resolve(KnownToken.ExpressHandler.context);
const logHandler = resolve(KnownToken.Http.Fetch.Middleware.Log.handler);
const cookieStore = resolve(KnownToken.Http.Fetch.cookieStore);
const abortController = resolve(KnownToken.Http.Fetch.abortController);

return [
// ВАЖНО: слой логирования ошибки ПЕРЕД остальными слоями чтобы не упустить ошибки выше
getFetchErrorLogging(logHandler),

// пробрасываемые заголовки по соглашению
defaultHeaders(getForwardedHeaders(config, context.req)),

// обрывание по сигналу из обработчика входящего запроса и по сигналу из конфига исходящего запроса
getFetchExtraAborting(abortController),

cookie(cookieStore),

getFetchTracing(tracer, context.res.locals.tracing.rootContext),

// ВАЖНО: слой логирования запроса и ответа ПОСЛЕ остальных слоев чтобы использовать актуальные данные
getFetchLogging(logHandler),
];
}
114 changes: 114 additions & 0 deletions src/preset/node-handler/providers/handler-main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { Resolve } from '../../../di';
import { KnownToken } from '../../../tokens';
import { renderToString } from 'react-dom/server';
import { PAGE_HANDLER_EVENT_TYPE } from '../../server';
import { HelmetContext } from '../../server/utils/regular-helmet';
import { getPageResponseFormat } from '../../node/utils/get-page-response-format';
import { ConventionalJson } from '../../isomorphic';
import { ResponseError } from '../../../http';

/**
* Провайдер главной функции обработчика входящего http-запроса.
* @param resolve Функция для получения зависимости по токену.
* @return Главная функция.
*/
export function provideHandlerMain(resolve: Resolve): VoidFunction {
const config = resolve(KnownToken.Config.base);
const logger = resolve(KnownToken.logger);
const assetsInit = resolve(KnownToken.Http.Handler.Page.assets);
const render = resolve(KnownToken.Http.Handler.Page.render);
const extras = resolve(KnownToken.Http.Handler.Response.specificExtras);
const Helmet = resolve(KnownToken.Http.Handler.Page.helmet);
const { req, res } = resolve(KnownToken.ExpressHandler.context);
const abortController = resolve(KnownToken.Http.Fetch.abortController);

const getAssets = typeof assetsInit === 'function' ? assetsInit : () => assetsInit;

/**
* Рендер JSX-элемента в строку.
* @param element Элемент.
* @return Строка.
*/
const elementToString = (element: JSX.Element) => {
res.emit(PAGE_HANDLER_EVENT_TYPE.renderStart);
const result = renderToString(element);
res.emit(PAGE_HANDLER_EVENT_TYPE.renderFinish);

return result;
};

return async () => {
try {
const assets = await getAssets();
const meta = extras.getMeta();

const jsx = (
<HelmetContext.Provider value={{ title: config.appName, assets }}>
<Helmet>{await render()}</Helmet>
</HelmetContext.Provider>
);

switch (getPageResponseFormat(req)) {
case 'html': {
res.setHeader('simaland-bundle-js', assets.js);
res.setHeader('simaland-bundle-css', assets.css);

if (assets.criticalJs) {
res.setHeader('simaland-critical-js', assets.criticalJs);
}

if (assets.criticalCss) {
res.setHeader('simaland-critical-css', assets.criticalCss);
}

if (meta) {
res.setHeader('simaland-meta', JSON.stringify(meta));
}

// ВАЖНО: DOCTYPE обязательно нужен так как влияет на то как браузер будет парсить html/css
// ВАЖНО: DOCTYPE нужен только когда отдаем полноценную страницу
if (config.env === 'development') {
res.send(`<!DOCTYPE html>${elementToString(jsx)}`);
} else {
res.send(elementToString(jsx));
}
break;
}

case 'json': {
res.json({
markup: elementToString(jsx),
bundle_js: assets.js,
bundle_css: assets.css,
critical_js: assets.criticalJs,
critical_css: assets.criticalCss,
meta,
} satisfies ConventionalJson);
break;
}
}
} catch (error) {
let message: string;
let statusCode = 500; // по умолчанию, если на этапе подготовки страницы что-то не так, отдаем 500

if (error instanceof Error) {
message = error.message;

if (error instanceof ResponseError) {
statusCode = error.statusCode;
}
} else {
message = String(error);
}

res.status(statusCode).send(message);
logger.error(error);
}

// ВАЖНО: прерываем исходящие в рамках обработчика http-запросы
abortController.abort();
};
}

// @todo а что если привести все зависимости к виду:
// const getAppConfig = resolve.lazy(KnownToken.Config.base);
Loading

0 comments on commit 3065a97

Please sign in to comment.