@tramvai/module-common 3.21.0 → 3.24.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/lib/CommonModule.browser.js +1 -0
- package/lib/CommonModule.es.js +1 -0
- package/lib/CommonModule.js +1 -0
- package/lib/actions/actionPageRunner.es.js +35 -4
- package/lib/actions/actionPageRunner.js +34 -3
- package/lib/async-local-storage/server.es.js +1 -1
- package/lib/async-local-storage/server.js +1 -1
- package/lib/cache/CacheModule.browser.js +3 -6
- package/lib/cache/CacheModule.es.js +3 -6
- package/lib/cache/CacheModule.js +2 -5
- package/lib/providers/serverProviders.d.ts +13 -1
- package/lib/providers/serverProviders.es.js +148 -1
- package/lib/providers/serverProviders.js +148 -1
- package/package.json +18 -18
package/lib/CommonModule.es.js
CHANGED
package/lib/CommonModule.js
CHANGED
|
@@ -98,6 +98,7 @@ exports.CommonModule = tslib.__decorate([
|
|
|
98
98
|
}),
|
|
99
99
|
core.provide({
|
|
100
100
|
provide: tokensCommon.EXECUTION_CONTEXT_MANAGER_TOKEN,
|
|
101
|
+
scope: core.Scope.SINGLETON,
|
|
101
102
|
useClass: executionContextManager.ExecutionContextManager,
|
|
102
103
|
}),
|
|
103
104
|
...serverProviders.providers,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ACTION_PARAMETERS, isTramvaiAction } from '@tramvai/core';
|
|
2
|
-
import { isSilentError } from '@tinkoff/errors';
|
|
2
|
+
import { ExecutionAbortError, isSilentError } from '@tinkoff/errors';
|
|
3
3
|
import { actionServerStateEvent } from './actionTramvaiReducer.es.js';
|
|
4
4
|
import { generateDeferredResolve, generateDeferredReject } from './deferred/clientScriptsUtils.es.js';
|
|
5
5
|
|
|
@@ -17,7 +17,18 @@ class ActionPageRunner {
|
|
|
17
17
|
return this.deps.executionContextManager.withContext(this.deps.commandLineExecutionContext(), { name: 'pageActions', values: { pageActions: true } }, (executionContext, abortController) => {
|
|
18
18
|
return new Promise((resolve, reject) => {
|
|
19
19
|
const timeoutMarker = setTimeout(() => {
|
|
20
|
-
|
|
20
|
+
const unfinishedActions = [];
|
|
21
|
+
this.deps.actionExecution.execution.forEach((value, key) => {
|
|
22
|
+
if (value.status === 'pending') {
|
|
23
|
+
unfinishedActions.push(key);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
this.log.warn({
|
|
27
|
+
event: `actions-execution-timeout`,
|
|
28
|
+
message: `Page actions has exceeded timeout of ${this.deps.limitTime}ms, ignore some results of execution.
|
|
29
|
+
You can find more detailed information from "action-execution-error" logs, and find relative logs by using the same "x-request-id" header`,
|
|
30
|
+
unfinishedActions,
|
|
31
|
+
});
|
|
21
32
|
abortController.abort();
|
|
22
33
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
23
34
|
endChecker();
|
|
@@ -68,19 +79,39 @@ class ActionPageRunner {
|
|
|
68
79
|
});
|
|
69
80
|
}
|
|
70
81
|
return promise;
|
|
82
|
+
})
|
|
83
|
+
.then((payload) => {
|
|
84
|
+
var _a;
|
|
85
|
+
if (executionContext.abortSignal.aborted) {
|
|
86
|
+
const parameters = isTramvaiAction(action) ? action : action[ACTION_PARAMETERS];
|
|
87
|
+
const actionName = (_a = parameters === null || parameters === void 0 ? void 0 : parameters.name) !== null && _a !== void 0 ? _a : 'unknown';
|
|
88
|
+
const contextName = `${executionContext.name}.${actionName}`;
|
|
89
|
+
this.log.warn({
|
|
90
|
+
error: new ExecutionAbortError({
|
|
91
|
+
message: `Execution aborted in context "${contextName}"`,
|
|
92
|
+
contextName,
|
|
93
|
+
}),
|
|
94
|
+
event: `action-execution-error`,
|
|
95
|
+
message: `${actionName} has exceeded timeout of ${this.deps.limitTime}ms, execution results will be ignored.
|
|
96
|
+
This action will be automatically executed on client - https://tramvai.dev/docs/features/data-fetching/action#synchronizing-between-server-and-client
|
|
97
|
+
If the request in this action takes too long, you can move it to the client using "onlyBrowser" condition or use Deferred Actions.
|
|
98
|
+
Also, the necessary network accesses may not be present.`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return payload;
|
|
71
102
|
})
|
|
72
103
|
.catch((error) => {
|
|
73
104
|
var _a;
|
|
74
105
|
const isCriticalError = stopRunAtError(error);
|
|
75
106
|
if (!isSilentError(error)) {
|
|
76
107
|
const parameters = isTramvaiAction(action) ? action : action[ACTION_PARAMETERS];
|
|
77
|
-
this.log.
|
|
108
|
+
this.log.warn({
|
|
78
109
|
error,
|
|
79
110
|
event: `action-execution-error`,
|
|
80
111
|
message: `${(_a = parameters === null || parameters === void 0 ? void 0 : parameters.name) !== null && _a !== void 0 ? _a : 'unknown'} execution error, ${isCriticalError
|
|
81
112
|
? `${error.name} error are expected and will stop actions execution and prevent page rendering`
|
|
82
113
|
: `this action will be automatically executed on client - https://tramvai.dev/docs/features/data-fetching/action#synchronizing-between-server-and-client
|
|
83
|
-
If the request in this action takes too long, you can move it to the client using "onlyBrowser" condition.
|
|
114
|
+
If the request in this action takes too long, you can move it to the client using "onlyBrowser" condition or use Deferred Actions.
|
|
84
115
|
Also, the necessary network accesses may not be present.`}`,
|
|
85
116
|
});
|
|
86
117
|
}
|
|
@@ -21,7 +21,18 @@ class ActionPageRunner {
|
|
|
21
21
|
return this.deps.executionContextManager.withContext(this.deps.commandLineExecutionContext(), { name: 'pageActions', values: { pageActions: true } }, (executionContext, abortController) => {
|
|
22
22
|
return new Promise((resolve, reject) => {
|
|
23
23
|
const timeoutMarker = setTimeout(() => {
|
|
24
|
-
|
|
24
|
+
const unfinishedActions = [];
|
|
25
|
+
this.deps.actionExecution.execution.forEach((value, key) => {
|
|
26
|
+
if (value.status === 'pending') {
|
|
27
|
+
unfinishedActions.push(key);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
this.log.warn({
|
|
31
|
+
event: `actions-execution-timeout`,
|
|
32
|
+
message: `Page actions has exceeded timeout of ${this.deps.limitTime}ms, ignore some results of execution.
|
|
33
|
+
You can find more detailed information from "action-execution-error" logs, and find relative logs by using the same "x-request-id" header`,
|
|
34
|
+
unfinishedActions,
|
|
35
|
+
});
|
|
25
36
|
abortController.abort();
|
|
26
37
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
27
38
|
endChecker();
|
|
@@ -72,19 +83,39 @@ class ActionPageRunner {
|
|
|
72
83
|
});
|
|
73
84
|
}
|
|
74
85
|
return promise;
|
|
86
|
+
})
|
|
87
|
+
.then((payload) => {
|
|
88
|
+
var _a;
|
|
89
|
+
if (executionContext.abortSignal.aborted) {
|
|
90
|
+
const parameters = core.isTramvaiAction(action) ? action : action[core.ACTION_PARAMETERS];
|
|
91
|
+
const actionName = (_a = parameters === null || parameters === void 0 ? void 0 : parameters.name) !== null && _a !== void 0 ? _a : 'unknown';
|
|
92
|
+
const contextName = `${executionContext.name}.${actionName}`;
|
|
93
|
+
this.log.warn({
|
|
94
|
+
error: new errors.ExecutionAbortError({
|
|
95
|
+
message: `Execution aborted in context "${contextName}"`,
|
|
96
|
+
contextName,
|
|
97
|
+
}),
|
|
98
|
+
event: `action-execution-error`,
|
|
99
|
+
message: `${actionName} has exceeded timeout of ${this.deps.limitTime}ms, execution results will be ignored.
|
|
100
|
+
This action will be automatically executed on client - https://tramvai.dev/docs/features/data-fetching/action#synchronizing-between-server-and-client
|
|
101
|
+
If the request in this action takes too long, you can move it to the client using "onlyBrowser" condition or use Deferred Actions.
|
|
102
|
+
Also, the necessary network accesses may not be present.`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return payload;
|
|
75
106
|
})
|
|
76
107
|
.catch((error) => {
|
|
77
108
|
var _a;
|
|
78
109
|
const isCriticalError = stopRunAtError(error);
|
|
79
110
|
if (!errors.isSilentError(error)) {
|
|
80
111
|
const parameters = core.isTramvaiAction(action) ? action : action[core.ACTION_PARAMETERS];
|
|
81
|
-
this.log.
|
|
112
|
+
this.log.warn({
|
|
82
113
|
error,
|
|
83
114
|
event: `action-execution-error`,
|
|
84
115
|
message: `${(_a = parameters === null || parameters === void 0 ? void 0 : parameters.name) !== null && _a !== void 0 ? _a : 'unknown'} execution error, ${isCriticalError
|
|
85
116
|
? `${error.name} error are expected and will stop actions execution and prevent page rendering`
|
|
86
117
|
: `this action will be automatically executed on client - https://tramvai.dev/docs/features/data-fetching/action#synchronizing-between-server-and-client
|
|
87
|
-
If the request in this action takes too long, you can move it to the client using "onlyBrowser" condition.
|
|
118
|
+
If the request in this action takes too long, you can move it to the client using "onlyBrowser" condition or use Deferred Actions.
|
|
88
119
|
Also, the necessary network accesses may not be present.`}`,
|
|
89
120
|
});
|
|
90
121
|
}
|
|
@@ -18,7 +18,7 @@ AsyncLocalStorageModule = __decorate([
|
|
|
18
18
|
provide({
|
|
19
19
|
provide: WEB_FASTIFY_APP_INIT_TOKEN,
|
|
20
20
|
multi: true,
|
|
21
|
-
scope: Scope.
|
|
21
|
+
scope: Scope.SINGLETON,
|
|
22
22
|
useFactory: ({ app, storage }) => {
|
|
23
23
|
return () => {
|
|
24
24
|
app.addHook('onRequest', (req, reply, done) => {
|
|
@@ -22,7 +22,7 @@ exports.AsyncLocalStorageModule = tslib.__decorate([
|
|
|
22
22
|
core.provide({
|
|
23
23
|
provide: tokensServerPrivate.WEB_FASTIFY_APP_INIT_TOKEN,
|
|
24
24
|
multi: true,
|
|
25
|
-
scope: core.Scope.
|
|
25
|
+
scope: core.Scope.SINGLETON,
|
|
26
26
|
useFactory: ({ app, storage }) => {
|
|
27
27
|
return () => {
|
|
28
28
|
app.addHook('onRequest', (req, reply, done) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { __decorate } from 'tslib';
|
|
2
2
|
import { createToken } from '@tinkoff/dippy';
|
|
3
|
-
import { Module, Scope
|
|
3
|
+
import { Module, Scope } from '@tramvai/core';
|
|
4
4
|
import { CREATE_CACHE_TOKEN, CLEAR_CACHE_TOKEN, REGISTER_CLEAR_CACHE_TOKEN } from '@tramvai/tokens-common';
|
|
5
5
|
import { cacheFactory } from './cacheFactory.browser.browser.js';
|
|
6
6
|
import { providers } from './clientProviders.browser.js';
|
|
@@ -19,10 +19,8 @@ CacheModule = __decorate([
|
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
provide: CREATE_CACHE_TOKEN,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return cacheFactory;
|
|
25
|
-
}
|
|
22
|
+
scope: Scope.SINGLETON,
|
|
23
|
+
useFactory: ({ caches }) => {
|
|
26
24
|
return (type, ...args) => {
|
|
27
25
|
const cache = cacheFactory(type, ...args);
|
|
28
26
|
caches.push(cache);
|
|
@@ -31,7 +29,6 @@ CacheModule = __decorate([
|
|
|
31
29
|
},
|
|
32
30
|
deps: {
|
|
33
31
|
caches: cachesToken,
|
|
34
|
-
isChildDi: { token: IS_DI_CHILD_CONTAINER_TOKEN, optional: true },
|
|
35
32
|
},
|
|
36
33
|
},
|
|
37
34
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { __decorate } from 'tslib';
|
|
2
2
|
import { createToken } from '@tinkoff/dippy';
|
|
3
|
-
import { Module, Scope
|
|
3
|
+
import { Module, Scope } from '@tramvai/core';
|
|
4
4
|
import { CREATE_CACHE_TOKEN, CLEAR_CACHE_TOKEN, REGISTER_CLEAR_CACHE_TOKEN } from '@tramvai/tokens-common';
|
|
5
5
|
import { cacheFactory } from './cacheFactory.es.js';
|
|
6
6
|
import { providers } from './serverProviders.es.js';
|
|
@@ -19,10 +19,8 @@ CacheModule = __decorate([
|
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
provide: CREATE_CACHE_TOKEN,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return cacheFactory;
|
|
25
|
-
}
|
|
22
|
+
scope: Scope.SINGLETON,
|
|
23
|
+
useFactory: ({ caches }) => {
|
|
26
24
|
return (type, ...args) => {
|
|
27
25
|
const cache = cacheFactory(type, ...args);
|
|
28
26
|
caches.push(cache);
|
|
@@ -31,7 +29,6 @@ CacheModule = __decorate([
|
|
|
31
29
|
},
|
|
32
30
|
deps: {
|
|
33
31
|
caches: cachesToken,
|
|
34
|
-
isChildDi: { token: IS_DI_CHILD_CONTAINER_TOKEN, optional: true },
|
|
35
32
|
},
|
|
36
33
|
},
|
|
37
34
|
{
|
package/lib/cache/CacheModule.js
CHANGED
|
@@ -23,10 +23,8 @@ exports.CacheModule = tslib.__decorate([
|
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
provide: tokensCommon.CREATE_CACHE_TOKEN,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return cacheFactory.cacheFactory;
|
|
29
|
-
}
|
|
26
|
+
scope: core.Scope.SINGLETON,
|
|
27
|
+
useFactory: ({ caches }) => {
|
|
30
28
|
return (type, ...args) => {
|
|
31
29
|
const cache = cacheFactory.cacheFactory(type, ...args);
|
|
32
30
|
caches.push(cache);
|
|
@@ -35,7 +33,6 @@ exports.CacheModule = tslib.__decorate([
|
|
|
35
33
|
},
|
|
36
34
|
deps: {
|
|
37
35
|
caches: cachesToken,
|
|
38
|
-
isChildDi: { token: core.IS_DI_CHILD_CONTAINER_TOKEN, optional: true },
|
|
39
36
|
},
|
|
40
37
|
},
|
|
41
38
|
{
|
|
@@ -1,2 +1,14 @@
|
|
|
1
|
-
export declare const providers:
|
|
1
|
+
export declare const providers: import("@tramvai/core").Provider<{
|
|
2
|
+
di: import("@tinkoff/dippy").Container & {
|
|
3
|
+
__type?: "base token" | undefined;
|
|
4
|
+
};
|
|
5
|
+
logger: import("@tramvai/tokens-common").Logger & ((configOrName: string | {
|
|
6
|
+
[key: string]: any;
|
|
7
|
+
name: string;
|
|
8
|
+
}) => import("@tramvai/tokens-common").Logger) & {
|
|
9
|
+
__type?: "base token" | undefined;
|
|
10
|
+
};
|
|
11
|
+
}, import("@tramvai/core").Command & {
|
|
12
|
+
__type?: "multi token" | undefined;
|
|
13
|
+
}>[];
|
|
2
14
|
//# sourceMappingURL=serverProviders.d.ts.map
|
|
@@ -1,3 +1,150 @@
|
|
|
1
|
-
|
|
1
|
+
import { provide, commandLineListTokens, DI_TOKEN } from '@tramvai/core';
|
|
2
|
+
import { LOGGER_TOKEN } from '@tramvai/tokens-common';
|
|
3
|
+
|
|
4
|
+
// transform "Symbol(token)" to human readable "token"
|
|
5
|
+
function tokenToString(token) {
|
|
6
|
+
return token.toString().replace(/^Symbol\((.+)\)$/, '$1');
|
|
7
|
+
}
|
|
8
|
+
function traverseUp(node, path, callback) {
|
|
9
|
+
const parents = Array.from(node.parents);
|
|
10
|
+
parents.forEach((parentNode) => {
|
|
11
|
+
const pathCopy = [...path];
|
|
12
|
+
pathCopy.push(node);
|
|
13
|
+
const stop = callback(parentNode, pathCopy) === false;
|
|
14
|
+
if (!stop) {
|
|
15
|
+
traverseUp(parentNode, pathCopy, callback);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Usage of "Request" scoped dependencies in "Singleton" providers can lead to errors,
|
|
21
|
+
* for example usage of `DispatcherContext` in `commandLineListTokens.init` is meaningless
|
|
22
|
+
* - for this case one specific `DispatcherContext` instance will be created and cached in `init` deps.
|
|
23
|
+
*
|
|
24
|
+
* In a future, we have plan to prohibit usage of "Request" dependencies inside "Singleton" providers.
|
|
25
|
+
* For now, we will detect such cases and log an error in development mode.
|
|
26
|
+
*/
|
|
27
|
+
function detectDiScopesCollisions({ di, logger, }) {
|
|
28
|
+
const log = logger('di-validator');
|
|
29
|
+
const collisions = new Map();
|
|
30
|
+
// eslint-disable-next-line prefer-destructuring
|
|
31
|
+
const diRecords = di.records;
|
|
32
|
+
const nodes = new Map();
|
|
33
|
+
diRecords.forEach((rootRecord, rootTokenName) => {
|
|
34
|
+
const rootTokenStr = tokenToString(rootTokenName);
|
|
35
|
+
if (rootRecord.multi) {
|
|
36
|
+
rootRecord.multi.forEach((rootMultiRecord) => {
|
|
37
|
+
processRootRecord(rootMultiRecord);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
processRootRecord(rootRecord);
|
|
42
|
+
}
|
|
43
|
+
function processRootRecord(rootRecord) {
|
|
44
|
+
if (!nodes.has(rootRecord)) {
|
|
45
|
+
nodes.set(rootRecord, {
|
|
46
|
+
name: rootTokenStr,
|
|
47
|
+
record: rootRecord,
|
|
48
|
+
parents: new Set(),
|
|
49
|
+
children: new Set(),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// create dependencies graph from flat list
|
|
53
|
+
walkOverDepsRecords(rootRecord, createAndConnectNodes);
|
|
54
|
+
// looking for request deps, used in singleton providers
|
|
55
|
+
walkOverDepsRecords(rootRecord, checkCollision);
|
|
56
|
+
}
|
|
57
|
+
function createAndConnectNodes(name, record, rootRecord) {
|
|
58
|
+
if (!nodes.has(record)) {
|
|
59
|
+
nodes.set(record, {
|
|
60
|
+
name,
|
|
61
|
+
record,
|
|
62
|
+
parents: new Set(),
|
|
63
|
+
children: new Set(),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
nodes.get(record).parents.add(nodes.get(rootRecord));
|
|
67
|
+
nodes.get(rootRecord).children.add(nodes.get(record));
|
|
68
|
+
}
|
|
69
|
+
function checkCollision(name, record) {
|
|
70
|
+
if (record.scope !== 'request' || !record.factory || collisions.has(name)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
let collision = false;
|
|
74
|
+
let collisionPaths = [];
|
|
75
|
+
traverseUp(nodes.get(record), [], (node, paths) => {
|
|
76
|
+
if (collision) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const isSingleton = node.record.scope === 'singleton';
|
|
80
|
+
if (isSingleton) {
|
|
81
|
+
paths.push(node);
|
|
82
|
+
collision = true;
|
|
83
|
+
collisionPaths = paths.reverse();
|
|
84
|
+
// it is enough to detect first collision and stop traverse
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
if (collision) {
|
|
89
|
+
if (!collisions.has(name)) {
|
|
90
|
+
collisions.set(name, new Set());
|
|
91
|
+
}
|
|
92
|
+
collisions.get(name).add({ record, paths: collisionPaths });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function walkOverDepsRecords(rootRecord, callback) {
|
|
96
|
+
var _a;
|
|
97
|
+
const resolvedDepsList = Object.values((_a = rootRecord.resolvedDeps) !== null && _a !== void 0 ? _a : {});
|
|
98
|
+
resolvedDepsList.forEach((token) => {
|
|
99
|
+
const tokenObj = token.token || token;
|
|
100
|
+
const tokenName = typeof tokenObj === 'string' ? Symbol.for(tokenObj) : tokenObj.name;
|
|
101
|
+
const tokenStr = tokenToString(tokenName);
|
|
102
|
+
const tokenRecord = di.getRecord(tokenName);
|
|
103
|
+
if (!tokenRecord) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (tokenRecord.multi) {
|
|
107
|
+
tokenRecord.multi.forEach((multiRecord) => {
|
|
108
|
+
callback(tokenStr, multiRecord, rootRecord);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
callback(tokenStr, tokenRecord, rootRecord);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
collisions.forEach((records, key) => {
|
|
118
|
+
var _a, _b, _c, _d, _e;
|
|
119
|
+
const recordsArr = Array.from(records.values());
|
|
120
|
+
const { record, paths } = (_a = recordsArr[0]) !== null && _a !== void 0 ? _a : {};
|
|
121
|
+
let message = `\nIncorrect usage of "${key}" - token has Request scope, but requested in Singleton scoped provider.
|
|
122
|
+
Requested dependency path: \n ${paths.map((path) => path.name).join(' -> ')}\n`;
|
|
123
|
+
if (record.stack) {
|
|
124
|
+
message += `Request token stack trace: \n ${(_c = (_b = record.stack.split('\n')[1]) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : 'unknown'}`;
|
|
125
|
+
}
|
|
126
|
+
const root = paths[0];
|
|
127
|
+
if (root.record.stack) {
|
|
128
|
+
message += `\nSingleton token stack trace: \n ${(_e = (_d = root.record.stack.split('\n')[1]) === null || _d === void 0 ? void 0 : _d.trim()) !== null && _e !== void 0 ? _e : 'unknown'}`;
|
|
129
|
+
}
|
|
130
|
+
log.error(message);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const providers = process.env.NODE_ENV === 'development'
|
|
134
|
+
? [
|
|
135
|
+
provide({
|
|
136
|
+
provide: commandLineListTokens.listen,
|
|
137
|
+
useFactory: (deps) => {
|
|
138
|
+
return () => {
|
|
139
|
+
detectDiScopesCollisions(deps);
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
deps: {
|
|
143
|
+
di: DI_TOKEN,
|
|
144
|
+
logger: LOGGER_TOKEN,
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
]
|
|
148
|
+
: [];
|
|
2
149
|
|
|
3
150
|
export { providers };
|
|
@@ -2,6 +2,153 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
var core = require('@tramvai/core');
|
|
6
|
+
var tokensCommon = require('@tramvai/tokens-common');
|
|
7
|
+
|
|
8
|
+
// transform "Symbol(token)" to human readable "token"
|
|
9
|
+
function tokenToString(token) {
|
|
10
|
+
return token.toString().replace(/^Symbol\((.+)\)$/, '$1');
|
|
11
|
+
}
|
|
12
|
+
function traverseUp(node, path, callback) {
|
|
13
|
+
const parents = Array.from(node.parents);
|
|
14
|
+
parents.forEach((parentNode) => {
|
|
15
|
+
const pathCopy = [...path];
|
|
16
|
+
pathCopy.push(node);
|
|
17
|
+
const stop = callback(parentNode, pathCopy) === false;
|
|
18
|
+
if (!stop) {
|
|
19
|
+
traverseUp(parentNode, pathCopy, callback);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Usage of "Request" scoped dependencies in "Singleton" providers can lead to errors,
|
|
25
|
+
* for example usage of `DispatcherContext` in `commandLineListTokens.init` is meaningless
|
|
26
|
+
* - for this case one specific `DispatcherContext` instance will be created and cached in `init` deps.
|
|
27
|
+
*
|
|
28
|
+
* In a future, we have plan to prohibit usage of "Request" dependencies inside "Singleton" providers.
|
|
29
|
+
* For now, we will detect such cases and log an error in development mode.
|
|
30
|
+
*/
|
|
31
|
+
function detectDiScopesCollisions({ di, logger, }) {
|
|
32
|
+
const log = logger('di-validator');
|
|
33
|
+
const collisions = new Map();
|
|
34
|
+
// eslint-disable-next-line prefer-destructuring
|
|
35
|
+
const diRecords = di.records;
|
|
36
|
+
const nodes = new Map();
|
|
37
|
+
diRecords.forEach((rootRecord, rootTokenName) => {
|
|
38
|
+
const rootTokenStr = tokenToString(rootTokenName);
|
|
39
|
+
if (rootRecord.multi) {
|
|
40
|
+
rootRecord.multi.forEach((rootMultiRecord) => {
|
|
41
|
+
processRootRecord(rootMultiRecord);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
processRootRecord(rootRecord);
|
|
46
|
+
}
|
|
47
|
+
function processRootRecord(rootRecord) {
|
|
48
|
+
if (!nodes.has(rootRecord)) {
|
|
49
|
+
nodes.set(rootRecord, {
|
|
50
|
+
name: rootTokenStr,
|
|
51
|
+
record: rootRecord,
|
|
52
|
+
parents: new Set(),
|
|
53
|
+
children: new Set(),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// create dependencies graph from flat list
|
|
57
|
+
walkOverDepsRecords(rootRecord, createAndConnectNodes);
|
|
58
|
+
// looking for request deps, used in singleton providers
|
|
59
|
+
walkOverDepsRecords(rootRecord, checkCollision);
|
|
60
|
+
}
|
|
61
|
+
function createAndConnectNodes(name, record, rootRecord) {
|
|
62
|
+
if (!nodes.has(record)) {
|
|
63
|
+
nodes.set(record, {
|
|
64
|
+
name,
|
|
65
|
+
record,
|
|
66
|
+
parents: new Set(),
|
|
67
|
+
children: new Set(),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
nodes.get(record).parents.add(nodes.get(rootRecord));
|
|
71
|
+
nodes.get(rootRecord).children.add(nodes.get(record));
|
|
72
|
+
}
|
|
73
|
+
function checkCollision(name, record) {
|
|
74
|
+
if (record.scope !== 'request' || !record.factory || collisions.has(name)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
let collision = false;
|
|
78
|
+
let collisionPaths = [];
|
|
79
|
+
traverseUp(nodes.get(record), [], (node, paths) => {
|
|
80
|
+
if (collision) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const isSingleton = node.record.scope === 'singleton';
|
|
84
|
+
if (isSingleton) {
|
|
85
|
+
paths.push(node);
|
|
86
|
+
collision = true;
|
|
87
|
+
collisionPaths = paths.reverse();
|
|
88
|
+
// it is enough to detect first collision and stop traverse
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
if (collision) {
|
|
93
|
+
if (!collisions.has(name)) {
|
|
94
|
+
collisions.set(name, new Set());
|
|
95
|
+
}
|
|
96
|
+
collisions.get(name).add({ record, paths: collisionPaths });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function walkOverDepsRecords(rootRecord, callback) {
|
|
100
|
+
var _a;
|
|
101
|
+
const resolvedDepsList = Object.values((_a = rootRecord.resolvedDeps) !== null && _a !== void 0 ? _a : {});
|
|
102
|
+
resolvedDepsList.forEach((token) => {
|
|
103
|
+
const tokenObj = token.token || token;
|
|
104
|
+
const tokenName = typeof tokenObj === 'string' ? Symbol.for(tokenObj) : tokenObj.name;
|
|
105
|
+
const tokenStr = tokenToString(tokenName);
|
|
106
|
+
const tokenRecord = di.getRecord(tokenName);
|
|
107
|
+
if (!tokenRecord) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (tokenRecord.multi) {
|
|
111
|
+
tokenRecord.multi.forEach((multiRecord) => {
|
|
112
|
+
callback(tokenStr, multiRecord, rootRecord);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
callback(tokenStr, tokenRecord, rootRecord);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
collisions.forEach((records, key) => {
|
|
122
|
+
var _a, _b, _c, _d, _e;
|
|
123
|
+
const recordsArr = Array.from(records.values());
|
|
124
|
+
const { record, paths } = (_a = recordsArr[0]) !== null && _a !== void 0 ? _a : {};
|
|
125
|
+
let message = `\nIncorrect usage of "${key}" - token has Request scope, but requested in Singleton scoped provider.
|
|
126
|
+
Requested dependency path: \n ${paths.map((path) => path.name).join(' -> ')}\n`;
|
|
127
|
+
if (record.stack) {
|
|
128
|
+
message += `Request token stack trace: \n ${(_c = (_b = record.stack.split('\n')[1]) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : 'unknown'}`;
|
|
129
|
+
}
|
|
130
|
+
const root = paths[0];
|
|
131
|
+
if (root.record.stack) {
|
|
132
|
+
message += `\nSingleton token stack trace: \n ${(_e = (_d = root.record.stack.split('\n')[1]) === null || _d === void 0 ? void 0 : _d.trim()) !== null && _e !== void 0 ? _e : 'unknown'}`;
|
|
133
|
+
}
|
|
134
|
+
log.error(message);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const providers = process.env.NODE_ENV === 'development'
|
|
138
|
+
? [
|
|
139
|
+
core.provide({
|
|
140
|
+
provide: core.commandLineListTokens.listen,
|
|
141
|
+
useFactory: (deps) => {
|
|
142
|
+
return () => {
|
|
143
|
+
detectDiScopesCollisions(deps);
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
deps: {
|
|
147
|
+
di: core.DI_TOKEN,
|
|
148
|
+
logger: tokensCommon.LOGGER_TOKEN,
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
]
|
|
152
|
+
: [];
|
|
6
153
|
|
|
7
154
|
exports.providers = providers;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tramvai/module-common",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.24.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
@@ -37,28 +37,28 @@
|
|
|
37
37
|
"@tinkoff/pubsub": "0.6.1",
|
|
38
38
|
"@tinkoff/url": "0.9.2",
|
|
39
39
|
"@tramvai/safe-strings": "0.6.2",
|
|
40
|
-
"@tramvai/experiments": "3.
|
|
41
|
-
"@tramvai/module-cookie": "3.
|
|
42
|
-
"@tramvai/module-environment": "3.
|
|
43
|
-
"@tramvai/module-log": "3.
|
|
44
|
-
"@tramvai/tokens-child-app": "3.
|
|
45
|
-
"@tramvai/tokens-core-private": "3.
|
|
46
|
-
"@tramvai/tokens-common": "3.
|
|
47
|
-
"@tramvai/tokens-render": "3.
|
|
48
|
-
"@tramvai/tokens-server-private": "3.
|
|
49
|
-
"@tramvai/types-actions-state-context": "3.
|
|
40
|
+
"@tramvai/experiments": "3.24.0",
|
|
41
|
+
"@tramvai/module-cookie": "3.24.0",
|
|
42
|
+
"@tramvai/module-environment": "3.24.0",
|
|
43
|
+
"@tramvai/module-log": "3.24.0",
|
|
44
|
+
"@tramvai/tokens-child-app": "3.24.0",
|
|
45
|
+
"@tramvai/tokens-core-private": "3.24.0",
|
|
46
|
+
"@tramvai/tokens-common": "3.24.0",
|
|
47
|
+
"@tramvai/tokens-render": "3.24.0",
|
|
48
|
+
"@tramvai/tokens-server-private": "3.24.0",
|
|
49
|
+
"@tramvai/types-actions-state-context": "3.24.0",
|
|
50
50
|
"hoist-non-react-statics": "^3.3.1",
|
|
51
51
|
"node-abort-controller": "^3.0.1"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@tinkoff/dippy": "0.9.
|
|
54
|
+
"@tinkoff/dippy": "0.9.2",
|
|
55
55
|
"@tinkoff/utils": "^2.1.2",
|
|
56
|
-
"@tramvai/cli": "3.
|
|
57
|
-
"@tramvai/core": "3.
|
|
58
|
-
"@tramvai/papi": "3.
|
|
59
|
-
"@tramvai/react": "3.
|
|
60
|
-
"@tramvai/state": "3.
|
|
61
|
-
"@tramvai/tokens-server": "3.
|
|
56
|
+
"@tramvai/cli": "3.24.0",
|
|
57
|
+
"@tramvai/core": "3.24.0",
|
|
58
|
+
"@tramvai/papi": "3.24.0",
|
|
59
|
+
"@tramvai/react": "3.24.0",
|
|
60
|
+
"@tramvai/state": "3.24.0",
|
|
61
|
+
"@tramvai/tokens-server": "3.24.0",
|
|
62
62
|
"react": ">=16.14.0",
|
|
63
63
|
"tslib": "^2.4.0"
|
|
64
64
|
},
|