From 43cb581879a48aa16a0cd2b82e10a1b49336dfd1 Mon Sep 17 00:00:00 2001 From: krutoo Date: Tue, 12 Mar 2024 16:35:19 +0500 Subject: [PATCH] #38 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - preset/bun-handler: модуль провайдеров разбит на отдельные модули по провайдерам (patch) --- src/preset/bun-handler/index.ts | 19 +- .../bun-handler/providers/cookie-store.ts | 10 + .../providers/fetch-log-handler.ts | 22 ++ .../bun-handler/providers/fetch-middleware.ts | 33 +++ .../bun-handler/providers/handler-main.tsx | 151 +++++++++++ src/preset/bun-handler/providers/index.tsx | 238 ------------------ .../bun-handler/providers/page-helmet.ts | 15 ++ .../bun-handler/providers/specific-params.ts | 15 ++ 8 files changed, 258 insertions(+), 245 deletions(-) create mode 100644 src/preset/bun-handler/providers/cookie-store.ts create mode 100644 src/preset/bun-handler/providers/fetch-log-handler.ts create mode 100644 src/preset/bun-handler/providers/fetch-middleware.ts create mode 100644 src/preset/bun-handler/providers/handler-main.tsx delete mode 100644 src/preset/bun-handler/providers/index.tsx create mode 100644 src/preset/bun-handler/providers/page-helmet.ts create mode 100644 src/preset/bun-handler/providers/specific-params.ts diff --git a/src/preset/bun-handler/index.ts b/src/preset/bun-handler/index.ts index cbc768e..0651630 100644 --- a/src/preset/bun-handler/index.ts +++ b/src/preset/bun-handler/index.ts @@ -6,8 +6,13 @@ import { provideAbortController } from '../isomorphic/providers/abort-controller import { provideFetch } from '../isomorphic/providers/fetch'; import { provideReduxMiddlewareSaga } from '../isomorphic/providers/redux-middleware-saga'; import { providePageRender } from '../node-handler/providers/page-render'; +import { provideFetchLogHandler } from './providers/fetch-log-handler'; +import { provideFetchMiddleware } from './providers/fetch-middleware'; +import { provideHandlerMain } from './providers/handler-main'; +import { providePageHelmet } from './providers/page-helmet'; +import { provideSpecificParams } from './providers/specific-params'; +import { provideCookieStore } from './providers/cookie-store'; import { SpecificExtras } from '../server/utils/specific-extras'; -import { HandlerProviders } from './providers'; /** * Возвращает preset с зависимостями для формирования обработчика входящего http-запроса. @@ -20,16 +25,16 @@ export function PresetBunHandler(customize?: PresetTuner) { // http fetch preset.set(KnownToken.Http.fetch, provideFetch); preset.set(KnownToken.Http.Fetch.abortController, provideAbortController); - preset.set(KnownToken.Http.Fetch.cookieStore, HandlerProviders.cookieStore); - preset.set(KnownToken.Http.Fetch.middleware, HandlerProviders.fetchMiddleware); - preset.set(KnownToken.Http.Fetch.Middleware.Log.handler, HandlerProviders.fetchLogHandler); + preset.set(KnownToken.Http.Fetch.cookieStore, provideCookieStore); + preset.set(KnownToken.Http.Fetch.middleware, provideFetchMiddleware); + preset.set(KnownToken.Http.Fetch.Middleware.Log.handler, provideFetchLogHandler); // handler - preset.set(KnownToken.Http.Handler.main, HandlerProviders.handlerMain); - preset.set(KnownToken.Http.Handler.Request.specificParams, HandlerProviders.specificParams); + preset.set(KnownToken.Http.Handler.main, provideHandlerMain); + preset.set(KnownToken.Http.Handler.Request.specificParams, provideSpecificParams); preset.set(KnownToken.Http.Handler.Response.specificExtras, () => new SpecificExtras()); preset.set(KnownToken.Http.Handler.Page.assets, () => ({ js: '', css: '' })); - preset.set(KnownToken.Http.Handler.Page.helmet, HandlerProviders.pageHelmet); + preset.set(KnownToken.Http.Handler.Page.helmet, providePageHelmet); preset.set(KnownToken.Http.Handler.Page.render, providePageRender); // redux saga diff --git a/src/preset/bun-handler/providers/cookie-store.ts b/src/preset/bun-handler/providers/cookie-store.ts new file mode 100644 index 0000000..3004b0c --- /dev/null +++ b/src/preset/bun-handler/providers/cookie-store.ts @@ -0,0 +1,10 @@ +/* eslint-disable require-jsdoc, jsdoc/require-jsdoc */ +import { Resolve } from '../../../di'; +import { KnownToken } from '../../../tokens'; +import { CookieStore, createCookieStore } from '../../../http'; + +export function provideCookieStore(resolve: Resolve): CookieStore { + const context = resolve(KnownToken.Http.Handler.context); + + return createCookieStore(context.request.headers.get('cookie') ?? undefined); +} diff --git a/src/preset/bun-handler/providers/fetch-log-handler.ts b/src/preset/bun-handler/providers/fetch-log-handler.ts new file mode 100644 index 0000000..2b004e3 --- /dev/null +++ b/src/preset/bun-handler/providers/fetch-log-handler.ts @@ -0,0 +1,22 @@ +/* eslint-disable require-jsdoc, jsdoc/require-jsdoc */ +import { Resolve } from '../../../di'; +import { KnownToken } from '../../../tokens'; +import { LogHandler, LogHandlerFactory } from '../../../http'; +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; +} diff --git a/src/preset/bun-handler/providers/fetch-middleware.ts b/src/preset/bun-handler/providers/fetch-middleware.ts new file mode 100644 index 0000000..169e0ae --- /dev/null +++ b/src/preset/bun-handler/providers/fetch-middleware.ts @@ -0,0 +1,33 @@ +/* eslint-disable require-jsdoc, jsdoc/require-jsdoc */ +import { Resolve } from '../../../di'; +import { KnownToken } from '../../../tokens'; +import { Middleware, cookie, defaultHeaders } from '../../../http'; +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 { getForwardedHeaders } from '../../server/utils/get-forwarded-headers'; + +export function provideFetchMiddleware(resolve: Resolve): Middleware[] { + const config = resolve(KnownToken.Config.base); + const context = resolve(KnownToken.Http.Handler.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), + + // обрывание по сигналу из обработчика входящего запроса и по сигналу из конфига исходящего запроса + getFetchExtraAborting(abortController), + + cookie(cookieStore), + + defaultHeaders(getForwardedHeaders(config, context.request)), + + // @todo tracing + + // ВАЖНО: слой логирования запроса и ответа ПОСЛЕ остальных слоев чтобы использовать актуальные данные + getFetchLogging(logHandler), + ]; +} diff --git a/src/preset/bun-handler/providers/handler-main.tsx b/src/preset/bun-handler/providers/handler-main.tsx new file mode 100644 index 0000000..3859a84 --- /dev/null +++ b/src/preset/bun-handler/providers/handler-main.tsx @@ -0,0 +1,151 @@ +/* eslint-disable require-jsdoc, jsdoc/require-jsdoc */ +import { renderToString } from 'react-dom/server'; +import { Resolve } from '../../../di'; +import { KnownToken } from '../../../tokens'; +import { ResponseError, applyMiddleware } from '../../../http'; +import { PageAssets } from '../../isomorphic/types'; +import { PAGE_HANDLER_EVENT_TYPE } from '../../server/constants'; +import { getPageResponseFormat } from '../../server/utils/get-page-response-format'; +import { HelmetContext } from '../../server/utils/regular-helmet'; + +export function provideHandlerMain(resolve: Resolve) { + 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 abortController = resolve(KnownToken.Http.Fetch.abortController); + const context = resolve(KnownToken.Http.Handler.context); + + // @todo https://github.com/sima-land/isomorph/issues/69 + // const cookieStore = resolve(KnownToken.Http.Fetch.cookieStore); + // const forwardedSetCookie: string[] = []; + // const unsubscribeCookieStore = cookieStore.subscribe(setCookieList => { + // forwardedSetCookie.push(...setCookieList); + // }); + + const getAssets = typeof assetsInit === 'function' ? assetsInit : () => assetsInit; + + const elementToString = (element: JSX.Element) => { + context.events.dispatchEvent(new Event(PAGE_HANDLER_EVENT_TYPE.renderStart)); + const result = renderToString(element); + context.events.dispatchEvent(new Event(PAGE_HANDLER_EVENT_TYPE.renderFinish)); + + return result; + }; + + const getResponseHTML = (jsx: React.JSX.Element, assets: PageAssets, meta: unknown) => { + const headers = new Headers(); + + headers.set('content-type', 'text/html'); + headers.set('simaland-bundle-js', assets.js); + headers.set('simaland-bundle-css', assets.css); + + if (assets.criticalJs) { + headers.set('simaland-critical-js', assets.criticalJs); + } + + if (assets.criticalCss) { + headers.set('simaland-critical-css', assets.criticalCss); + } + + if (meta) { + headers.set('simaland-meta', JSON.stringify(meta)); + } + + // ВАЖНО: DOCTYPE обязательно нужен так как влияет на то как браузер будет парсить html/css + // ВАЖНО: DOCTYPE нужен только когда отдаем полноценную страницу + if (config.env === 'development') { + return new Response(`${elementToString(jsx)}`, { + headers, + }); + } else { + return new Response(elementToString(jsx), { + headers, + }); + } + }; + + const getResponseJSON = (jsx: React.JSX.Element, assets: PageAssets, meta: unknown) => { + const headers = new Headers(); + + headers.set('content-type', 'application/json'); + + return new Response( + JSON.stringify({ + markup: elementToString(jsx), + bundle_js: assets.js, + bundle_css: assets.css, + critical_js: assets.criticalJs, + critical_css: assets.criticalCss, + meta, + }), + { headers }, + ); + }; + + const handler = async (request: Request): Promise => { + try { + const assets = await getAssets(); + const meta = extras.getMeta(); + + const jsx = ( + + {await render()} + + ); + + switch (getPageResponseFormat(request)) { + case 'html': { + return getResponseHTML(jsx, assets, meta); + } + case 'json': { + return getResponseJSON(jsx, assets, meta); + } + } + } 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); + } + + logger.error(error); + + return new Response(message, { + status: statusCode, + }); + } + }; + + const enhancer = applyMiddleware( + // @todo https://github.com/sima-land/isomorph/issues/69 + // async (request, next) => { + // const response = await next(request); + // for (const item of forwardedSetCookie) { + // response.headers.append('set-cookie', item); + // } + // unsubscribeCookieStore(); + // return response; + // }, + + // ВАЖНО: прерываем исходящие в рамках обработчика http-запросы + async (request, next) => { + const response = await next(request); + + abortController.abort(); + + return response; + }, + ); + + return enhancer(handler); +} diff --git a/src/preset/bun-handler/providers/index.tsx b/src/preset/bun-handler/providers/index.tsx deleted file mode 100644 index 4ba864b..0000000 --- a/src/preset/bun-handler/providers/index.tsx +++ /dev/null @@ -1,238 +0,0 @@ -/* eslint-disable require-jsdoc, jsdoc/require-jsdoc */ -import { renderToString } from 'react-dom/server'; -import { Resolve } from '../../../di'; -import { KnownToken } from '../../../tokens'; -import { - CookieStore, - Middleware, - ResponseError, - applyMiddleware, - cookie, - createCookieStore, - defaultHeaders, - LogHandler, - LogHandlerFactory, -} from '../../../http'; -import { Fragment } from 'react'; -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 { FetchLogging } from '../../isomorphic/utils/fetch-logging'; -import { PageAssets } from '../../isomorphic/types'; -import { PAGE_HANDLER_EVENT_TYPE } from '../../server/constants'; -import { HelmetContext, RegularHelmet } from '../../server/utils/regular-helmet'; -import { getPageResponseFormat } from '../../server/utils/get-page-response-format'; -import { getForwardedHeaders } from '../../server/utils/get-forwarded-headers'; - -export const HandlerProviders = { - handlerMain(resolve: Resolve) { - 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 abortController = resolve(KnownToken.Http.Fetch.abortController); - const context = resolve(KnownToken.Http.Handler.context); - - // @todo https://github.com/sima-land/isomorph/issues/69 - // const cookieStore = resolve(KnownToken.Http.Fetch.cookieStore); - // const forwardedSetCookie: string[] = []; - // const unsubscribeCookieStore = cookieStore.subscribe(setCookieList => { - // forwardedSetCookie.push(...setCookieList); - // }); - - const getAssets = typeof assetsInit === 'function' ? assetsInit : () => assetsInit; - - const elementToString = (element: JSX.Element) => { - context.events.dispatchEvent(new Event(PAGE_HANDLER_EVENT_TYPE.renderStart)); - const result = renderToString(element); - context.events.dispatchEvent(new Event(PAGE_HANDLER_EVENT_TYPE.renderFinish)); - - return result; - }; - - const getResponseHTML = (jsx: React.JSX.Element, assets: PageAssets, meta: unknown) => { - const headers = new Headers(); - - headers.set('content-type', 'text/html'); - headers.set('simaland-bundle-js', assets.js); - headers.set('simaland-bundle-css', assets.css); - - if (assets.criticalJs) { - headers.set('simaland-critical-js', assets.criticalJs); - } - - if (assets.criticalCss) { - headers.set('simaland-critical-css', assets.criticalCss); - } - - if (meta) { - headers.set('simaland-meta', JSON.stringify(meta)); - } - - // ВАЖНО: DOCTYPE обязательно нужен так как влияет на то как браузер будет парсить html/css - // ВАЖНО: DOCTYPE нужен только когда отдаем полноценную страницу - if (config.env === 'development') { - return new Response(`${elementToString(jsx)}`, { - headers, - }); - } else { - return new Response(elementToString(jsx), { - headers, - }); - } - }; - - const getResponseJSON = (jsx: React.JSX.Element, assets: PageAssets, meta: unknown) => { - const headers = new Headers(); - - headers.set('content-type', 'application/json'); - - return new Response( - JSON.stringify({ - markup: elementToString(jsx), - bundle_js: assets.js, - bundle_css: assets.css, - critical_js: assets.criticalJs, - critical_css: assets.criticalCss, - meta, - }), - { headers }, - ); - }; - - const handler = async (request: Request): Promise => { - try { - const assets = await getAssets(); - const meta = extras.getMeta(); - - const jsx = ( - - {await render()} - - ); - - switch (getPageResponseFormat(request)) { - case 'html': { - return getResponseHTML(jsx, assets, meta); - } - case 'json': { - return getResponseJSON(jsx, assets, meta); - } - } - } 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); - } - - logger.error(error); - - return new Response(message, { - status: statusCode, - }); - } - }; - - const enhancer = applyMiddleware( - // @todo https://github.com/sima-land/isomorph/issues/69 - // async (request, next) => { - // const response = await next(request); - // for (const item of forwardedSetCookie) { - // response.headers.append('set-cookie', item); - // } - // unsubscribeCookieStore(); - // return response; - // }, - - // ВАЖНО: прерываем исходящие в рамках обработчика http-запросы - async (request, next) => { - const response = await next(request); - - abortController.abort(); - - return response; - }, - ); - - return enhancer(handler); - }, - - pageHelmet(resolve: Resolve) { - const config = resolve(KnownToken.Config.base); - const { request } = resolve(KnownToken.Http.Handler.context); - - return config.env === 'development' && getPageResponseFormat(request) === 'html' - ? RegularHelmet - : Fragment; - }, - - specificParams(resolve: Resolve) { - const context = resolve(KnownToken.Http.Handler.context); - - try { - const headerValue = context.request.headers.get('simaland-params'); - - return JSON.parse(headerValue ?? '{}'); - } catch { - return {}; - } - }, - - fetchMiddleware(resolve: Resolve): Middleware[] { - const config = resolve(KnownToken.Config.base); - const context = resolve(KnownToken.Http.Handler.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), - - // обрывание по сигналу из обработчика входящего запроса и по сигналу из конфига исходящего запроса - getFetchExtraAborting(abortController), - - cookie(cookieStore), - - defaultHeaders(getForwardedHeaders(config, context.request)), - - // @todo tracing - - // ВАЖНО: слой логирования запроса и ответа ПОСЛЕ остальных слоев чтобы использовать актуальные данные - getFetchLogging(logHandler), - ]; - }, - - /** - * Провайдер обработчика логирования axios. - * @param resolve Функция для получения зависимости по токену. - * @return Обработчик логирования. - */ - fetchLogHandler(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; - }, - - cookieStore(resolve: Resolve): CookieStore { - const context = resolve(KnownToken.Http.Handler.context); - - return createCookieStore(context.request.headers.get('cookie') ?? undefined); - }, -} as const; diff --git a/src/preset/bun-handler/providers/page-helmet.ts b/src/preset/bun-handler/providers/page-helmet.ts new file mode 100644 index 0000000..6ff4195 --- /dev/null +++ b/src/preset/bun-handler/providers/page-helmet.ts @@ -0,0 +1,15 @@ +/* eslint-disable require-jsdoc, jsdoc/require-jsdoc */ +import { Resolve } from '../../../di'; +import { KnownToken } from '../../../tokens'; +import { Fragment } from 'react'; +import { RegularHelmet } from '../../server/utils/regular-helmet'; +import { getPageResponseFormat } from '../../server/utils/get-page-response-format'; + +export function providePageHelmet(resolve: Resolve) { + const config = resolve(KnownToken.Config.base); + const { request } = resolve(KnownToken.Http.Handler.context); + + return config.env === 'development' && getPageResponseFormat(request) === 'html' + ? RegularHelmet + : Fragment; +} diff --git a/src/preset/bun-handler/providers/specific-params.ts b/src/preset/bun-handler/providers/specific-params.ts new file mode 100644 index 0000000..a732246 --- /dev/null +++ b/src/preset/bun-handler/providers/specific-params.ts @@ -0,0 +1,15 @@ +/* eslint-disable require-jsdoc, jsdoc/require-jsdoc */ +import { Resolve } from '../../../di'; +import { KnownToken } from '../../../tokens'; + +export function provideSpecificParams(resolve: Resolve) { + const context = resolve(KnownToken.Http.Handler.context); + + try { + const headerValue = context.request.headers.get('simaland-params'); + + return JSON.parse(headerValue ?? '{}'); + } catch { + return {}; + } +}