@tramvai/module-server 2.72.4 → 2.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -186,6 +186,31 @@ To see values that related to request look for the header `Server-Timing` or che
186
186
 
187
187
  Module uses loggers with identifiers: `server`, `server:static`, `server:webapp`, `server:node-debug:request`
188
188
 
189
+ ### Early Hints
190
+
191
+ Module send the [103 Early Hints](https://developer.chrome.com/blog/early-hints) response to provide better performance, though there are several limitations (look [here](https://chromium.googlesource.com/chromium/src/+/master/docs/early-hints.md#what_s-not-supported) and [here](https://developer.chrome.com/blog/early-hints/#current-limitations)). Currently, module provides hints for next resources:
192
+
193
+ - Resources with `preconnectLink` type;
194
+ - General resources, which have the `preloadLink` type;
195
+ - Page resources, that exist in the `ResourcesRegistry` with `'data-critical': "true"` attribute;
196
+ - Preconnects for CDN urls, that are computed from `preloadLink` or `data-critical` resources, described above;
197
+
198
+ Server will try to response with 103 as soon as possible and there can be more than one such responses.
199
+
200
+ ```shell
201
+ curl -I -X HEAD http://localhost:3000
202
+
203
+ HTTP/1.1 103 Early Hints
204
+ Link: <https://www.cdn-tinkoff.ru>; rel=preconnect
205
+ Link: <https://www.cdn-tinkoff.ru/frontend-libraries/npm/react-kit-font/1.0.0/TinkoffSans.woff2>; rel=preload; as=font
206
+ Link: <https://www-stage.cdn-tinkoff.ru/frontend-libraries/feedback/1.14.0/feedback.css>; rel=preload; as=style
207
+ Link: <https://www-stage.cdn-tinkoff.ru>; rel=preconnect
208
+
209
+ HTTP/1.1 200 OK
210
+ ...
211
+
212
+ ```
213
+
189
214
  ## How to
190
215
 
191
216
  ### Setting `keepAliveTimeout` for the server
@@ -0,0 +1,2 @@
1
+ export declare class EarlyHintsModule {
2
+ }
@@ -0,0 +1,84 @@
1
+ import { __decorate } from 'tslib';
2
+ import { Module, provide, commandLineListTokens } from '@tramvai/core';
3
+ import { EARLY_HINTS_MANAGER_TOKEN, FASTIFY_RESPONSE } from '@tramvai/tokens-server-private';
4
+ import { RESOURCES_REGISTRY, RENDER_FLOW_AFTER_TOKEN } from '@tramvai/tokens-render';
5
+ import { EarlyHintsManager } from './service.es.js';
6
+
7
+ let EarlyHintsModule = class EarlyHintsModule {
8
+ };
9
+ EarlyHintsModule = __decorate([
10
+ Module({
11
+ providers: [
12
+ provide({
13
+ provide: EARLY_HINTS_MANAGER_TOKEN,
14
+ useClass: EarlyHintsManager,
15
+ deps: {
16
+ response: FASTIFY_RESPONSE,
17
+ resourcesRegistry: RESOURCES_REGISTRY,
18
+ },
19
+ }),
20
+ provide({
21
+ provide: commandLineListTokens.customerStart,
22
+ multi: true,
23
+ useFactory: ({ earlyHints }) => {
24
+ return async function writeCommonEarlyHints() {
25
+ await earlyHints.flushHints();
26
+ };
27
+ },
28
+ deps: {
29
+ earlyHints: EARLY_HINTS_MANAGER_TOKEN,
30
+ },
31
+ }),
32
+ provide({
33
+ provide: commandLineListTokens.resolvePageDeps,
34
+ multi: true,
35
+ useFactory: ({ earlyHints }) => {
36
+ return async function writeCommonEarlyHints() {
37
+ await earlyHints.flushHints();
38
+ };
39
+ },
40
+ deps: {
41
+ earlyHints: EARLY_HINTS_MANAGER_TOKEN,
42
+ },
43
+ }),
44
+ provide({
45
+ provide: commandLineListTokens.resolveUserDeps,
46
+ multi: true,
47
+ useFactory: ({ earlyHints }) => {
48
+ return async function writeCommonEarlyHints() {
49
+ await earlyHints.flushHints();
50
+ };
51
+ },
52
+ deps: {
53
+ earlyHints: EARLY_HINTS_MANAGER_TOKEN,
54
+ },
55
+ }),
56
+ provide({
57
+ provide: commandLineListTokens.generatePage,
58
+ multi: true,
59
+ useFactory: ({ earlyHints }) => {
60
+ return async function writeCommonEarlyHints() {
61
+ await earlyHints.flushHints();
62
+ };
63
+ },
64
+ deps: {
65
+ earlyHints: EARLY_HINTS_MANAGER_TOKEN,
66
+ },
67
+ }),
68
+ provide({
69
+ provide: RENDER_FLOW_AFTER_TOKEN,
70
+ multi: true,
71
+ useFactory: ({ earlyHints }) => {
72
+ return async function writePageEarlyHints() {
73
+ await earlyHints.flushHints();
74
+ };
75
+ },
76
+ deps: {
77
+ earlyHints: EARLY_HINTS_MANAGER_TOKEN,
78
+ },
79
+ }),
80
+ ],
81
+ })
82
+ ], EarlyHintsModule);
83
+
84
+ export { EarlyHintsModule };
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var tslib = require('tslib');
6
+ var core = require('@tramvai/core');
7
+ var tokensServerPrivate = require('@tramvai/tokens-server-private');
8
+ var tokensRender = require('@tramvai/tokens-render');
9
+ var service = require('./service.js');
10
+
11
+ exports.EarlyHintsModule = class EarlyHintsModule {
12
+ };
13
+ exports.EarlyHintsModule = tslib.__decorate([
14
+ core.Module({
15
+ providers: [
16
+ core.provide({
17
+ provide: tokensServerPrivate.EARLY_HINTS_MANAGER_TOKEN,
18
+ useClass: service.EarlyHintsManager,
19
+ deps: {
20
+ response: tokensServerPrivate.FASTIFY_RESPONSE,
21
+ resourcesRegistry: tokensRender.RESOURCES_REGISTRY,
22
+ },
23
+ }),
24
+ core.provide({
25
+ provide: core.commandLineListTokens.customerStart,
26
+ multi: true,
27
+ useFactory: ({ earlyHints }) => {
28
+ return async function writeCommonEarlyHints() {
29
+ await earlyHints.flushHints();
30
+ };
31
+ },
32
+ deps: {
33
+ earlyHints: tokensServerPrivate.EARLY_HINTS_MANAGER_TOKEN,
34
+ },
35
+ }),
36
+ core.provide({
37
+ provide: core.commandLineListTokens.resolvePageDeps,
38
+ multi: true,
39
+ useFactory: ({ earlyHints }) => {
40
+ return async function writeCommonEarlyHints() {
41
+ await earlyHints.flushHints();
42
+ };
43
+ },
44
+ deps: {
45
+ earlyHints: tokensServerPrivate.EARLY_HINTS_MANAGER_TOKEN,
46
+ },
47
+ }),
48
+ core.provide({
49
+ provide: core.commandLineListTokens.resolveUserDeps,
50
+ multi: true,
51
+ useFactory: ({ earlyHints }) => {
52
+ return async function writeCommonEarlyHints() {
53
+ await earlyHints.flushHints();
54
+ };
55
+ },
56
+ deps: {
57
+ earlyHints: tokensServerPrivate.EARLY_HINTS_MANAGER_TOKEN,
58
+ },
59
+ }),
60
+ core.provide({
61
+ provide: core.commandLineListTokens.generatePage,
62
+ multi: true,
63
+ useFactory: ({ earlyHints }) => {
64
+ return async function writeCommonEarlyHints() {
65
+ await earlyHints.flushHints();
66
+ };
67
+ },
68
+ deps: {
69
+ earlyHints: tokensServerPrivate.EARLY_HINTS_MANAGER_TOKEN,
70
+ },
71
+ }),
72
+ core.provide({
73
+ provide: tokensRender.RENDER_FLOW_AFTER_TOKEN,
74
+ multi: true,
75
+ useFactory: ({ earlyHints }) => {
76
+ return async function writePageEarlyHints() {
77
+ await earlyHints.flushHints();
78
+ };
79
+ },
80
+ deps: {
81
+ earlyHints: tokensServerPrivate.EARLY_HINTS_MANAGER_TOKEN,
82
+ },
83
+ }),
84
+ ],
85
+ })
86
+ ], exports.EarlyHintsModule);
@@ -0,0 +1,23 @@
1
+ import type { FASTIFY_RESPONSE, EARLY_HINTS_MANAGER_TOKEN } from '@tramvai/tokens-server-private';
2
+ import type { RESOURCES_REGISTRY } from '@tramvai/tokens-render';
3
+ type EarlyHintsInterface = typeof EARLY_HINTS_MANAGER_TOKEN;
4
+ type Response = typeof FASTIFY_RESPONSE;
5
+ type ResourcesRegistry = typeof RESOURCES_REGISTRY;
6
+ interface ConstructorPayload {
7
+ response: Response;
8
+ resourcesRegistry: ResourcesRegistry;
9
+ }
10
+ export declare class EarlyHintsManager implements EarlyHintsInterface {
11
+ private sentHints;
12
+ private readonly response;
13
+ private readonly resourcesRegistry;
14
+ constructor(payload: ConstructorPayload);
15
+ flushHints(): Promise<void>;
16
+ private getHints;
17
+ private writeToSocket;
18
+ private getHttpMessage;
19
+ private getAsAttribute;
20
+ private getCdnHintForResource;
21
+ private doesHintUniq;
22
+ }
23
+ export {};
@@ -0,0 +1,78 @@
1
+ class EarlyHintsManager {
2
+ constructor(payload) {
3
+ this.sentHints = new Set();
4
+ this.response = payload.response;
5
+ this.resourcesRegistry = payload.resourcesRegistry;
6
+ }
7
+ async flushHints() {
8
+ const hints = this.getHints();
9
+ await this.writeToSocket(hints);
10
+ }
11
+ getHints() {
12
+ return this.resourcesRegistry.getPageResources().reduce((acc, resource) => {
13
+ var _a, _b;
14
+ let resourceHint = null;
15
+ let cdnHint = null;
16
+ if (resource.type === 'preconnectLink') {
17
+ resourceHint = `Link: <${resource.payload}>; rel=preconnect`;
18
+ }
19
+ if (resource.type === 'preloadLink') {
20
+ const as = this.getAsAttribute(resource);
21
+ resourceHint = `Link: <${resource.payload}>; rel=preload;${as}`;
22
+ cdnHint = this.getCdnHintForResource(resource);
23
+ }
24
+ if (resource.type === 'style' && ((_a = resource.attrs) === null || _a === void 0 ? void 0 : _a['data-critical']) === 'true') {
25
+ resourceHint = `Link: <${resource.payload}>; rel=preload; as=style`;
26
+ cdnHint = this.getCdnHintForResource(resource);
27
+ }
28
+ // prevent scripts preloading because of potential large numbers of JS chunks for every page
29
+ // still check JS chunks for preconnects
30
+ if (resource.type === 'script' && ((_b = resource.attrs) === null || _b === void 0 ? void 0 : _b['data-critical']) === 'true') {
31
+ cdnHint = this.getCdnHintForResource(resource);
32
+ }
33
+ if (this.doesHintUniq(cdnHint)) {
34
+ acc.push(cdnHint);
35
+ this.sentHints.add(cdnHint);
36
+ }
37
+ if (this.doesHintUniq(resourceHint)) {
38
+ acc.push(resourceHint);
39
+ this.sentHints.add(resourceHint);
40
+ }
41
+ return acc;
42
+ }, []);
43
+ }
44
+ writeToSocket(hints) {
45
+ return new Promise((resolve) => {
46
+ const { socket } = this.response.raw;
47
+ // Socket will be null if the connection has been closed already
48
+ if (socket === null || hints.length === 0) {
49
+ resolve();
50
+ return;
51
+ }
52
+ const message = this.getHttpMessage(hints);
53
+ socket.write(message, 'ascii', () => {
54
+ resolve();
55
+ });
56
+ });
57
+ }
58
+ getHttpMessage(payload) {
59
+ const endOfLine = '\r\n';
60
+ const result = ['HTTP/1.1 103 Early Hints', ...payload];
61
+ // There must be an empty line between HTTP messages.
62
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages
63
+ return `${result.join(endOfLine)}${endOfLine.repeat(2)}`;
64
+ }
65
+ getAsAttribute(resource) {
66
+ var _a;
67
+ return ((_a = resource.attrs) === null || _a === void 0 ? void 0 : _a.as) !== undefined ? ` as=${resource.attrs.as}` : '';
68
+ }
69
+ getCdnHintForResource(resource) {
70
+ const match = resource.payload.match(/https:\/\/[^/'"]+/gi);
71
+ return match === null ? null : `Link: <${match[0]}>; rel=preconnect`;
72
+ }
73
+ doesHintUniq(hint) {
74
+ return hint !== null && !this.sentHints.has(hint);
75
+ }
76
+ }
77
+
78
+ export { EarlyHintsManager };
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ class EarlyHintsManager {
6
+ constructor(payload) {
7
+ this.sentHints = new Set();
8
+ this.response = payload.response;
9
+ this.resourcesRegistry = payload.resourcesRegistry;
10
+ }
11
+ async flushHints() {
12
+ const hints = this.getHints();
13
+ await this.writeToSocket(hints);
14
+ }
15
+ getHints() {
16
+ return this.resourcesRegistry.getPageResources().reduce((acc, resource) => {
17
+ var _a, _b;
18
+ let resourceHint = null;
19
+ let cdnHint = null;
20
+ if (resource.type === 'preconnectLink') {
21
+ resourceHint = `Link: <${resource.payload}>; rel=preconnect`;
22
+ }
23
+ if (resource.type === 'preloadLink') {
24
+ const as = this.getAsAttribute(resource);
25
+ resourceHint = `Link: <${resource.payload}>; rel=preload;${as}`;
26
+ cdnHint = this.getCdnHintForResource(resource);
27
+ }
28
+ if (resource.type === 'style' && ((_a = resource.attrs) === null || _a === void 0 ? void 0 : _a['data-critical']) === 'true') {
29
+ resourceHint = `Link: <${resource.payload}>; rel=preload; as=style`;
30
+ cdnHint = this.getCdnHintForResource(resource);
31
+ }
32
+ // prevent scripts preloading because of potential large numbers of JS chunks for every page
33
+ // still check JS chunks for preconnects
34
+ if (resource.type === 'script' && ((_b = resource.attrs) === null || _b === void 0 ? void 0 : _b['data-critical']) === 'true') {
35
+ cdnHint = this.getCdnHintForResource(resource);
36
+ }
37
+ if (this.doesHintUniq(cdnHint)) {
38
+ acc.push(cdnHint);
39
+ this.sentHints.add(cdnHint);
40
+ }
41
+ if (this.doesHintUniq(resourceHint)) {
42
+ acc.push(resourceHint);
43
+ this.sentHints.add(resourceHint);
44
+ }
45
+ return acc;
46
+ }, []);
47
+ }
48
+ writeToSocket(hints) {
49
+ return new Promise((resolve) => {
50
+ const { socket } = this.response.raw;
51
+ // Socket will be null if the connection has been closed already
52
+ if (socket === null || hints.length === 0) {
53
+ resolve();
54
+ return;
55
+ }
56
+ const message = this.getHttpMessage(hints);
57
+ socket.write(message, 'ascii', () => {
58
+ resolve();
59
+ });
60
+ });
61
+ }
62
+ getHttpMessage(payload) {
63
+ const endOfLine = '\r\n';
64
+ const result = ['HTTP/1.1 103 Early Hints', ...payload];
65
+ // There must be an empty line between HTTP messages.
66
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages
67
+ return `${result.join(endOfLine)}${endOfLine.repeat(2)}`;
68
+ }
69
+ getAsAttribute(resource) {
70
+ var _a;
71
+ return ((_a = resource.attrs) === null || _a === void 0 ? void 0 : _a.as) !== undefined ? ` as=${resource.attrs.as}` : '';
72
+ }
73
+ getCdnHintForResource(resource) {
74
+ const match = resource.payload.match(/https:\/\/[^/'"]+/gi);
75
+ return match === null ? null : `Link: <${match[0]}>; rel=preconnect`;
76
+ }
77
+ doesHintUniq(hint) {
78
+ return hint !== null && !this.sentHints.has(hint);
79
+ }
80
+ }
81
+
82
+ exports.EarlyHintsManager = EarlyHintsManager;
@@ -7,3 +7,4 @@ export * from './dependenciesVersion';
7
7
  export * from './utilityServer';
8
8
  export * from './keepAlive';
9
9
  export * from './serverTiming';
10
+ export * from './earlyHints';
package/lib/server.es.js CHANGED
@@ -22,6 +22,7 @@ import { DependenciesVersionModule } from './modules/dependenciesVersion.es.js';
22
22
  import { UtilityServerModule } from './modules/utilityServer.es.js';
23
23
  import { KeepAliveModule } from './modules/keepAlive.es.js';
24
24
  import { ServerTimingModule } from './modules/serverTiming.es.js';
25
+ import { EarlyHintsModule } from './modules/earlyHints/index.es.js';
25
26
 
26
27
  if (typeof setDefaultResultOrder === 'function') {
27
28
  setDefaultResultOrder('ipv4first');
@@ -44,6 +45,7 @@ ServerModule = __decorate([
44
45
  UtilityServerModule,
45
46
  KeepAliveModule,
46
47
  ServerTimingModule,
48
+ EarlyHintsModule,
47
49
  process.env.NODE_ENV !== 'production' && DebugHttpRequestsModule,
48
50
  ].filter(Boolean),
49
51
  providers: [
package/lib/server.js CHANGED
@@ -25,6 +25,7 @@ var dependenciesVersion = require('./modules/dependenciesVersion.js');
25
25
  var utilityServer = require('./modules/utilityServer.js');
26
26
  var keepAlive = require('./modules/keepAlive.js');
27
27
  var serverTiming = require('./modules/serverTiming.js');
28
+ var index = require('./modules/earlyHints/index.js');
28
29
 
29
30
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
30
31
 
@@ -51,6 +52,7 @@ exports.ServerModule = tslib.__decorate([
51
52
  utilityServer.UtilityServerModule,
52
53
  keepAlive.KeepAliveModule,
53
54
  serverTiming.ServerTimingModule,
55
+ index.EarlyHintsModule,
54
56
  process.env.NODE_ENV !== 'production' && debugRequests.DebugHttpRequestsModule,
55
57
  ].filter(Boolean),
56
58
  providers: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tramvai/module-server",
3
- "version": "2.72.4",
3
+ "version": "2.73.0",
4
4
  "description": "",
5
5
  "browser": "lib/browser.js",
6
6
  "main": "lib/server.js",
@@ -25,11 +25,11 @@
25
25
  "@tinkoff/monkeypatch": "2.0.4",
26
26
  "@tinkoff/terminus": "0.1.7",
27
27
  "@tinkoff/url": "0.8.5",
28
- "@tramvai/module-cache-warmup": "2.72.4",
29
- "@tramvai/module-metrics": "2.72.4",
30
- "@tramvai/papi": "2.72.4",
31
- "@tramvai/tokens-server": "2.72.4",
32
- "@tramvai/tokens-server-private": "2.72.4",
28
+ "@tramvai/module-cache-warmup": "2.73.0",
29
+ "@tramvai/module-metrics": "2.73.0",
30
+ "@tramvai/papi": "2.73.0",
31
+ "@tramvai/tokens-server": "2.73.0",
32
+ "@tramvai/tokens-server-private": "2.73.0",
33
33
  "fastify": "^4.6.0",
34
34
  "@fastify/cookie": "^8.1.0",
35
35
  "@fastify/compress": "^6.1.1",
@@ -40,12 +40,13 @@
40
40
  "peerDependencies": {
41
41
  "@tinkoff/dippy": "0.8.13",
42
42
  "@tinkoff/utils": "^2.1.2",
43
- "@tramvai/cli": "2.72.4",
44
- "@tramvai/core": "2.72.4",
45
- "@tramvai/module-common": "2.72.4",
46
- "@tramvai/module-environment": "2.72.4",
47
- "@tramvai/tokens-common": "2.72.4",
48
- "@tramvai/tokens-core-private": "2.72.4",
43
+ "@tramvai/cli": "2.73.0",
44
+ "@tramvai/core": "2.73.0",
45
+ "@tramvai/module-common": "2.73.0",
46
+ "@tramvai/module-environment": "2.73.0",
47
+ "@tramvai/tokens-common": "2.73.0",
48
+ "@tramvai/tokens-core-private": "2.73.0",
49
+ "@tramvai/tokens-render": "2.73.0",
49
50
  "tslib": "^2.4.0"
50
51
  },
51
52
  "devDependencies": {