How to marshall an i18next instance to helpers?
-
Assigning an i18n instance to req.data.i18n
I've been doing this without problems for a long time. The i18n instance is available to my helpers and translation works great. Suddenly I got into trouble with a specific report. I'm currently digging into this report, but I think the matter is bigger than one report.
When I switched from
in-process
todedicated-process
it got weird. Maybe it already was weird, but didn't show while usingin-process
.Here is how my (a bit simplified) beforeRender looks like:
async function beforeRender (req, res) { if(req.data && req.data.i18n) { const {languageCode, defaultNamespace, fallbackNamespace, namespaces} = req.data.i18n.init; const i18n = require('i18next').createInstance(); await i18n.init({ resources: req.data.i18n, interpolation: { escapeValue: true }, getAsync: false, //load resources synchronously. I don't know if necessary, but to be sure.. lng: languageCode, defaultNS: defaultNamespace, fallbackNS: fallbackNamespace, ns: namespaces, }); req.data.i18nInstance = i18n; } }
This seems to work fine (in-process). All translations works as they should. But I have one report which only works occasionally, most of the times I can render the report ONE time after a server restart. When it does not work, I don't get any error.
I have set debug level to "silly", but I still cannot see what happens. The last log output before it hangs is this:
debug: Rendering engine handlebars using in-process strategy
.Not working in dedicated-process
When I switch to
dedicated-process
, the report doesn't work at all. I get this:debug: Executing script dateTimeConstants using dedicated-process strategy (node:26) UnhandledPromiseRejectionWarning: TypeError: Converting circular structure to JSON --> starting at object with constructor 'Object' | property 'backendConnector' -> object with constructor 'Connector' --- property 'services' closes the circle at JSON.stringify (<anonymous>) at Object.module.exports.serialize (/app/node_modules/serializator/index.js:23:14) at Object.module.exports.serialize (/app/node_modules/script-manager/lib/messageHandler.js:4:23) at sendAndExit (/app/node_modules/script-manager/lib/worker-processes.js:43:33) at /app/node_modules/script-manager/lib/worker-processes.js:73:7 at doneWrap (/app/node_modules/jsreport-scripts/lib/scriptEvalChild.js:80:5) at /app/node_modules/jsreport-scripts/lib/scriptEvalChild.js:127:30 at processTicksAndRejections (internal/process/task_queues.js:97:5) (node:26) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1) (node:26) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
I think this has to do with serialising of the data object, but I'm not sure. Why would the whole data object need to be serialised? I know that there is serialisation going on when sending json data to child reports, but I didn't thing everything was serialised.
Serialisation problem?
Here is a similar problem: https://github.com/i18next/react-i18next/issues/440
The solution to this issue was to remove
toJSON()
from the i18n instance. But if I do that I cannot use i18n in my helpers anymore, i.e.options.data.root.i18nInstance
will not be available.So many questions
Q1, the obvious question: How to safely marshall an i81n instance to my helper functions?
Q2: Is req.data really serialised? If that were the case I wouldn't be able to use the i18n instance since functions cannot be serialised.
Q3: Why does it seem to be serialised in dedicated-process? (according to the error)
Q4: Why does my report often crash on second render when using in-process? The answer might be that my method of setting an i18n instance to req.data.i18n is flawed.
-
Q2: Is req.data really serialised? If that were the case I wouldn't be able to use the i18n instance since functions cannot be serialised.
Q3: Why does it seem to be serialised in dedicated-process? (according to the error)Yes, dedicated-process strategy means that it evaluates javascript in extra process. This means it needs to serialize
req.data
when communicating with it. The script and helper calls are using different process. This means they can share just plain objects.Q4: Why does my report often crash on second render when using in-process? The answer might be that my method of setting an i18n instance to req.data.i18n is flawed.
This should work. Because the instance is shared in the same process.
I tried something like this without problems. It works consistently with in-process strategy. You would probably need to isolate the problem.async function beforeRender(req, res) { const i18next = req.data.i18next = require('i18next').createInstance() await i18next.init({ lng: 'en', debug: true, resources: { en: { translation: { "key": "hello world" } } } }) }
Q1, the obvious question: How to safely marshall an i81n instance to my helper functions?
I think there is some kind of hacky way how to reconstruct the i81n instance from the serialized plain object. However, what looks more straightforward to me, is construct the i81n instance in the helpers section. This can be done because since jsreport 2.8.0 you can write asynchronous helpers.
const i18next = require('i18next').createInstance() const initPromise = i18next.init({ lng: 'en', debug: true, resources: { en: { translation: { "key": "hello world" } } } }) async function myHelper() { await initPromise return i18next.t('key') }
Then call the helper in common way
{{myHelper}}
-
That seems like a great solution. It always felt a bit awkward to put functions in the data object.
Q: How would I pass information to initPromise? I have resources, lng etc in req.data, as seen in my code above. When inside of a helper function I can use the extra last argument to get to my data object (options.data.root..)
Would this solution be ok?
const initPromise = (meta) => i18next.init({ lng: meta.languageCode, resources: meta.resources }) async function myHelper(options) { await initPromise(options.data.root.i18nMeta) return i18next.t('key') }
Maybe a stupid question, but here goes: Will
{{myHelper}}
in the template actually make anawait myHelper()
call? I suppose it wouldn't make sense to make the function async if it wasn't awaited for, but I want to understand.Also, I assume calling an already resolved promise for every translation won't take any noticeable time?
-
Answering this myself: It works like a charm!
Thank you Jan, for an elegant way to initialise i18n directly in the helpers instead of tampering with req.data from beforeRender.
-
This post is deleted!
-
I think I have found the problem now. My solution is to do this in a better way, but the problem might point to a bug in jsreport.
I have a child report which needs some data. I used to send this data via
childTemplateSerializeData
, which has served me well for all my reports. The problematic report did call this subreport many more times than any other report. In this particular example the child was called 66 times and the call was done with two separatechildTemplateSerializeData
like this:{#child child_blueprint_with_controlpoint @template.recipe=html @data.drawing$={{{childTemplateSerializeData ../this}}} @data.controlpoint$={{{childTemplateSerializeData ./this}}} @data.zoomSize={{getBlueprintZoomSize ../this}} }
I have now refactored the child report so it can be called like this:
{#child child_blueprint_with_controlpoint2 @template.recipe=html @data.drawingIndex={{@root.drawingIndex}} @data.controlpointIndex={{5}} @data.zoomSize={{getBlueprintZoomSize ../this}} }
I haven't encountered the bug since this refactoring. My new way of calling the child report is of course much better, but I do wonder why I got into trouble with childTemplateSerializeData.
- Could there be a problem when two separate parameters are using childTemplateSerializeData?
- Or does childTemplateSerialize data leak in some way that prohibits two consecutive renders?
Jan, if you want to investigate this I could guide you to recreate the problem in the template export you already got earlier today. As for me, I'll keep a healthy distance to childTemplateSerializeData =)
-
Oh, and there seems to be a bug in the forum as well. The above code came out of this original text in the post:
The index variable (prepended with @} is exchanged for a zero by the forum.
-
Could there be a problem when two separate parameters are using childTemplateSerializeData?
I don't see any problem with that.
Or does childTemplateSerialize data leak in some way that prohibits two consecutive renders?
Also not aware of that.
I tried it here and it seems to work as expected
https://playground.jsreport.net/w/anon/eW6NUT1eIt would be great if you can send me an updated export or guide me on how to replicate the problem.