Skip to content

Commit

Permalink
Merge pull request #103 from sima-land/38-examples-bun
Browse files Browse the repository at this point in the history
 Шаг 56 #38
  • Loading branch information
krutoo committed Feb 29, 2024
2 parents 55af82b + 6e0f4f0 commit e30483d
Show file tree
Hide file tree
Showing 14 changed files with 237 additions and 80 deletions.
2 changes: 1 addition & 1 deletion src/preset/bun/bun/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function PresetBun(customize?: PresetTuner) {
// @todo

// metrics
// @todo
preset.set(KnownToken.Metrics.httpHandler, BunProviders.serveMetrics);

// http fetch
preset.set(KnownToken.Http.fetch, BunProviders.fetch);
Expand Down
116 changes: 111 additions & 5 deletions src/preset/bun/bun/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ import { Resolve } from '../../../../di';
import { KnownToken } from '../../../../tokens';
import { ConfigSource, createConfigSource } from '../../../../config';
import { Logger, createLogger } from '../../../../log';
import { Handler, Middleware, applyMiddleware, log } from '../../../../http';
import { Handler, Middleware, log } from '../../../../http';
import { ServeLogging } from '../utils';
import { providePinoHandler } from '../../../node/node/providers';
import { route, router } from '@krutoo/fetch-tools';
import { getCurrentHub, init, runWithAsyncContext } from '@sentry/bun';
import { createSentryHandler } from '../../../../log/handler/sentry';
import { healthCheck } from '../../../isomorphic/utils';
import { provideFetch } from '../../../isomorphic/providers';
import { toMilliseconds } from '../../../../utils';
import { ServerMiddleware } from '../../../server/types';
import { applyServerMiddleware } from '../../../server/utils';
import PromClient from 'prom-client';
import { RESPONSE_EVENT_TYPE } from '../../../isomorphic/constants';

export const BunProviders = {
configSource(): ConfigSource {
Expand Down Expand Up @@ -54,22 +59,65 @@ export const BunProviders = {
const middleware = resolve(KnownToken.Http.Serve.middleware);
const routes = resolve(KnownToken.Http.Serve.routes);

const enhance = applyMiddleware(...middleware);
const enhance = applyServerMiddleware(...middleware);

return router(
// маршруты с промежуточными слоями
...routes.map(([pathname, handler]) => route(pathname, enhance(handler))),
...routes.map(([pathname, handler]) => {
const enhancedHandler = enhance(handler);
return route(pathname, request => enhancedHandler(request, { events: new EventTarget() }));
}),

// @todo вместо routes обрабатывать pageRoutes с помощью route.get() из новой версии fetch-tools (для явности)
// @todo также добавить apiRoutes и обрабатывать их с помощью с помощью route()?

// служебные маршруты (без промежуточных слоев)
route('/healthcheck', healthCheck()),
);
},

serveMiddleware(resolve: Resolve): Middleware[] {
serveMiddleware(resolve: Resolve): ServerMiddleware[] {
const config = resolve(KnownToken.Config.base);
const logger = resolve(KnownToken.logger);

const logging = new ServeLogging(logger);

// @todo перенести в preset/server
const ConventionalLabels = {
HTTP_RESPONSE: ['version', 'route', 'code', 'method'],
SSR: ['version', 'route', 'method'],
} as const;

const requestCount = new PromClient.Counter({
name: 'http_request_count',
help: 'Incoming HTTP request count',
labelNames: ConventionalLabels.HTTP_RESPONSE,
});

const responseDuration = new PromClient.Histogram({
name: 'http_response_duration_ms',
help: 'Duration of incoming HTTP requests in ms',
labelNames: ConventionalLabels.HTTP_RESPONSE,
buckets: [30, 100, 200, 500, 1000, 2500, 5000, 10000],
});

const renderDuration = new PromClient.Histogram({
name: 'render_duration_ms',
help: 'Duration of SSR ms',
labelNames: ConventionalLabels.SSR,
buckets: [0.1, 15, 50, 100, 250, 500, 800, 1500],
});

const getLabels = (
req: Request,
res: Response,
): Record<(typeof ConventionalLabels.HTTP_RESPONSE)[number], string | number> => ({
version: config.appVersion,
route: req.url,
code: res.status,
method: req.method,
});

return [
// ВАЖНО: изолируем хлебные крошки чтобы они группировались по входящим запросам
(request, next) => runWithAsyncContext(async () => next(request)),
Expand All @@ -79,7 +127,50 @@ export const BunProviders = {
onCatch: data => logging.onRequest(data),
}),

// @todo metrics, tracing
// @todo tracing

// метрики
async (request, next, context) => {
const responseStart = process.hrtime.bigint();
let renderStart = 0n;

context.events.addEventListener(
RESPONSE_EVENT_TYPE.renderStart,
() => {
renderStart = process.hrtime.bigint();
},
{ once: true },
);

context.events.addEventListener(
RESPONSE_EVENT_TYPE.renderFinish,
() => {
const renderFinish = process.hrtime.bigint();

renderDuration.observe(
{
version: config.appVersion,
method: request.method,
route: request.url,
},
toMilliseconds(renderFinish - renderStart),
);
},
{ once: true },
);

const response = await next(request);
const responseFinish = process.hrtime.bigint();

responseDuration.observe(
getLabels(request, response),
toMilliseconds(responseFinish - responseStart),
);

requestCount.inc(getLabels(request, response), 1);

return response;
},

// ВАЖНО: слой логирования запроса и ответа ПОСЛЕ остальных слоев чтобы использовать актуальные данные
log({
Expand All @@ -88,4 +179,19 @@ export const BunProviders = {
}),
];
},

serveMetrics(): Handler {
// @todo задействовать когда Bun реализует pref_hooks.monitorEventLoopDelay (https://github.com/siimon/prom-client/issues/570)
// PromClient.collectDefaultMetrics();

// @todo здесь или в другом компоненте надо проверять путь и метод запроса
return async () => {
const metrics = await PromClient.register.metrics();
const headers = new Headers();

headers.set('Content-Type', PromClient.register.contentType);

return new Response(metrics, { headers });
};
},
} as const;
104 changes: 57 additions & 47 deletions src/preset/bun/handler/providers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
import { Fragment } from 'react';
import { FetchLogging } from '../../../isomorphic/utils';
import { getForwardedHeaders, getPageResponseFormat } from '../../../server/utils';
import { PageAssets } from '../../../isomorphic/types';
import { RESPONSE_EVENT_TYPE } from '../../../isomorphic/constants';

export const HandlerProviders = {
handlerMain(resolve: Resolve) {
Expand All @@ -28,17 +30,68 @@ export const HandlerProviders = {
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);

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

const elementToString = (element: JSX.Element) => {
// @todo dispatch renderStart event for metrics/tracing
context.events.dispatchEvent(new Event(RESPONSE_EVENT_TYPE.renderStart));
const result = renderToString(element);
// @todo dispatch renderFinish event for metrics/tracing
context.events.dispatchEvent(new Event(RESPONSE_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(`<!DOCTYPE html>${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<Response> => {
try {
const assets = await getAssets();
Expand All @@ -52,53 +105,10 @@ export const HandlerProviders = {

switch (getPageResponseFormat(request)) {
case 'html': {
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(`<!DOCTYPE html>${elementToString(jsx)}`, {
headers,
});
} else {
return new Response(elementToString(jsx), {
headers,
});
}
return getResponseHTML(jsx, assets, meta);
}

case 'json': {
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 },
);
return getResponseJSON(jsx, assets, meta);
}
}
} catch (error) {
Expand Down
10 changes: 5 additions & 5 deletions src/preset/bun/handler/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
/* eslint-disable require-jsdoc, jsdoc/require-jsdoc */
import type { Handler } from '../../../../http';
import type { ServerHandler } from '../../../server/types';
import { KnownToken } from '../../../../tokens';
import { CURRENT_APP, type Application, type Resolve } from '../../../../di';

export function HandlerProvider(getApp: () => Application) {
return (resolve: Resolve): Handler => {
return (resolve: Resolve): ServerHandler => {
const parent = resolve(CURRENT_APP);

return request => {
return (request, context) => {
const app = getApp();

app.attach(parent);
app.bind(KnownToken.Http.Handler.context).toValue({ request });
app.bind(KnownToken.Http.Handler.context).toValue({ request, ...context });

return app.get(KnownToken.Http.Handler.main)(request);
return app.get(KnownToken.Http.Handler.main)(request, context);
};
};
}
8 changes: 4 additions & 4 deletions src/preset/node/handler/providers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { Resolve } from '../../../../di';
import { KnownToken } from '../../../../tokens';
import {
getForwardedHeaders as getForwardedHeadersExpress,
tracingMiddleware as tracingMiddlewareAxios,
axiosTracingMiddleware,
} from '../../node/utils/http-client';
import type { Middleware as AxiosMiddleware } from 'middleware-axios';
import { AxiosLogging, FetchLogging, HttpStatus } from '../../../isomorphic/utils';
Expand All @@ -22,7 +22,7 @@ import type { ConventionalJson } from '../../../isomorphic/types';
import { Fragment } from 'react';
import { HelmetContext, RegularHelmet, getPageResponseFormat } from '../utils';
import { renderToString } from 'react-dom/server';
import { tracingMiddleware } from '../../../server/utils';
import { fetchTracingMiddleware } from '../../../server/utils';

/**
* Провайдер главной функции обработчика входящего http-запроса.
Expand Down Expand Up @@ -201,7 +201,7 @@ export function provideFetchMiddleware(resolve: Resolve): Middleware[] {

cookie(cookieStore),

tracingMiddleware(tracer, context.res.locals.tracing.rootContext),
fetchTracingMiddleware(tracer, context.res.locals.tracing.rootContext),

// ВАЖНО: слой логирования запроса и ответа ПОСЛЕ остальных слоев чтобы использовать актуальные данные
log(initData => {
Expand Down Expand Up @@ -280,7 +280,7 @@ export function provideAxiosMiddleware(resolve: Resolve): AxiosMiddleware<any>[]
},

HttpStatus.axiosMiddleware(),
tracingMiddlewareAxios(tracer, context.res.locals.tracing.rootContext),
axiosTracingMiddleware(tracer, context.res.locals.tracing.rootContext),
logMiddleware(logHandler),
cookieMiddleware(cookieStore),
];
Expand Down
2 changes: 1 addition & 1 deletion src/preset/node/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export type { HandlerContext } from './types';
export type { ExpressHandlerContext } from './types';
export { PresetNode } from './node';
export { PresetHandler, HandlerProvider } from './handler';
Loading

0 comments on commit e30483d

Please sign in to comment.