@cubejs-backend/schema-compiler 1.7.47 → 1.7.49

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.
Files changed (50) hide show
  1. package/dist/src/adapter/BaseQuery.d.ts.map +1 -1
  2. package/dist/src/adapter/BaseQuery.js +2 -0
  3. package/dist/src/adapter/BaseQuery.js.map +1 -1
  4. package/dist/src/adapter/MssqlQuery.d.ts.map +1 -1
  5. package/dist/src/adapter/MssqlQuery.js +6 -0
  6. package/dist/src/adapter/MssqlQuery.js.map +1 -1
  7. package/dist/src/compiler/CubeEvaluator.d.ts +9 -1
  8. package/dist/src/compiler/CubeEvaluator.d.ts.map +1 -1
  9. package/dist/src/compiler/CubeEvaluator.js +19 -1
  10. package/dist/src/compiler/CubeEvaluator.js.map +1 -1
  11. package/dist/src/compiler/CubeSymbols.d.ts.map +1 -1
  12. package/dist/src/compiler/CubeSymbols.js +4 -0
  13. package/dist/src/compiler/CubeSymbols.js.map +1 -1
  14. package/dist/src/compiler/CubeToMetaTransformer.d.ts +4 -1
  15. package/dist/src/compiler/CubeToMetaTransformer.d.ts.map +1 -1
  16. package/dist/src/compiler/CubeToMetaTransformer.js +7 -1
  17. package/dist/src/compiler/CubeToMetaTransformer.js.map +1 -1
  18. package/dist/src/compiler/CubeValidator.d.ts.map +1 -1
  19. package/dist/src/compiler/CubeValidator.js +6 -0
  20. package/dist/src/compiler/CubeValidator.js.map +1 -1
  21. package/dist/src/compiler/DataSchemaCompiler.d.ts +7 -0
  22. package/dist/src/compiler/DataSchemaCompiler.d.ts.map +1 -1
  23. package/dist/src/compiler/DataSchemaCompiler.js +344 -7
  24. package/dist/src/compiler/DataSchemaCompiler.js.map +1 -1
  25. package/dist/src/compiler/PrepareCompiler.d.ts +6 -0
  26. package/dist/src/compiler/PrepareCompiler.d.ts.map +1 -1
  27. package/dist/src/compiler/PrepareCompiler.js +8 -3
  28. package/dist/src/compiler/PrepareCompiler.js.map +1 -1
  29. package/dist/src/compiler/StringInterning.d.ts +16 -0
  30. package/dist/src/compiler/StringInterning.d.ts.map +1 -0
  31. package/dist/src/compiler/StringInterning.js +106 -0
  32. package/dist/src/compiler/StringInterning.js.map +1 -0
  33. package/dist/src/compiler/index.d.ts +1 -0
  34. package/dist/src/compiler/index.d.ts.map +1 -1
  35. package/dist/src/compiler/index.js +5 -0
  36. package/dist/src/compiler/index.js.map +1 -1
  37. package/dist/src/compiler/transpilers/IIFETranspiler.d.ts.map +1 -1
  38. package/dist/src/compiler/transpilers/IIFETranspiler.js +8 -2
  39. package/dist/src/compiler/transpilers/IIFETranspiler.js.map +1 -1
  40. package/dist/src/compiler/transpilers/MemoKeyTranspiler.d.ts +11 -0
  41. package/dist/src/compiler/transpilers/MemoKeyTranspiler.d.ts.map +1 -0
  42. package/dist/src/compiler/transpilers/MemoKeyTranspiler.js +72 -0
  43. package/dist/src/compiler/transpilers/MemoKeyTranspiler.js.map +1 -0
  44. package/dist/src/compiler/transpilers/index.d.ts +1 -0
  45. package/dist/src/compiler/transpilers/index.d.ts.map +1 -1
  46. package/dist/src/compiler/transpilers/index.js +1 -0
  47. package/dist/src/compiler/transpilers/index.js.map +1 -1
  48. package/dist/src/compiler/transpilers/transpiler_worker.js +2 -0
  49. package/dist/src/compiler/transpilers/transpiler_worker.js.map +1 -1
  50. package/package.json +7 -7
@@ -18,7 +18,190 @@ const native_1 = require("@cubejs-backend/native");
18
18
  const UserError_1 = require("./UserError");
19
19
  const ErrorReporter_1 = require("./ErrorReporter");
20
20
  const CubeSymbols_1 = require("./CubeSymbols");
21
+ const transpilers_1 = require("./transpilers");
21
22
  const ctxFileStorage = new async_hooks_1.AsyncLocalStorage();
23
+ // Set while the function passed to `memo` runs, including its async continuations
24
+ const memoFnStorage = new async_hooks_1.AsyncLocalStorage();
25
+ // For memo() keys: objects compare by their keys whatever the property order, and NaN and
26
+ // Infinity don't encode as null
27
+ const memoKeyReplacer = (_key, value) => {
28
+ if (typeof value === 'number' && !Number.isFinite(value)) {
29
+ return { $memoNumber: String(value) };
30
+ }
31
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
32
+ return Object.fromEntries(Object.keys(value).sort().map((k) => [k, value[k]]));
33
+ }
34
+ return value;
35
+ };
36
+ // Shared realm: closures bind the running compile's scope lexically through `with`, so lazy reads
37
+ // (sql reading COMPILE_CONTEXT) keep resolving it after the compile; the inner function keeps
38
+ // top-level declarations off the shared global and gets the compile's own global as `this`.
39
+ const SHARED_SCOPE_GETTER = '__cubejsCompileScope';
40
+ const sharedScriptPrefix = (strict) => `with (${SHARED_SCOPE_GETTER}(${strict})) { (function () {`;
41
+ const SHARED_SCRIPT_SUFFIX = '\n}).call(globalThis); }';
42
+ // Whether the source opens with a 'use strict' directive, after whitespace and comments.
43
+ // A linear scan: a regex over repeated comments can backtrack exponentially.
44
+ function hasUseStrictDirective(source) {
45
+ let i = 0;
46
+ while (i < source.length) {
47
+ if (/\s/.test(source[i])) {
48
+ i++;
49
+ }
50
+ else if (source.startsWith('//', i)) {
51
+ const end = source.indexOf('\n', i);
52
+ if (end === -1)
53
+ return false;
54
+ i = end + 1;
55
+ }
56
+ else if (source.startsWith('/*', i)) {
57
+ const end = source.indexOf('*/', i + 2);
58
+ if (end === -1)
59
+ return false;
60
+ i = end + 2;
61
+ }
62
+ else {
63
+ break;
64
+ }
65
+ }
66
+ return source.startsWith("'use strict'", i) || source.startsWith('"use strict"', i);
67
+ }
68
+ // UMD wrappers probe these with `typeof`: read as undefined, not as a ReferenceError
69
+ const PROBED_GLOBALS = new Set(['module', 'exports', 'define']);
70
+ const sharedScopeStorage = new async_hooks_1.AsyncLocalStorage();
71
+ let sharedVmContext = null;
72
+ let sharedRealmGlobal = null;
73
+ const getSharedVmContext = () => {
74
+ if (!sharedVmContext) {
75
+ const sandbox = {};
76
+ Object.defineProperty(sandbox, SHARED_SCOPE_GETTER, {
77
+ value: (strict) => {
78
+ const compile = sharedScopeStorage.getStore();
79
+ if (!compile) {
80
+ throw new Error('No compile scope stored in context');
81
+ }
82
+ return strict ? compile.strictScope : compile.scope;
83
+ },
84
+ writable: false,
85
+ enumerable: false,
86
+ configurable: false,
87
+ });
88
+ const refuse = (key) => {
89
+ // The realm's own TypeError, as model code would catch it
90
+ throw new sharedRealmGlobal.TypeError(`Cannot set global '${String(key)}': data models share one realm with CUBEJS_COMPILER_MULTI_TENANT_SHARING`);
91
+ };
92
+ // What still reaches the real global (`this` of a nested sloppy function) fails loudly: a
93
+ // value stored there would be every tenant's.
94
+ sharedVmContext = vm_1.default.createContext(new Proxy(sandbox, {
95
+ set: (_t, key) => refuse(key),
96
+ defineProperty: (_t, key) => refuse(key),
97
+ deleteProperty: (_t, key) => refuse(key),
98
+ }));
99
+ sharedRealmGlobal = vm_1.default.runInContext('globalThis', sharedVmContext);
100
+ }
101
+ return sharedVmContext;
102
+ };
103
+ // Names the compile assigns (implicit globals, `globalThis.x`, `this.x`, even `JSON = ...`) land
104
+ // in `vars`; reads fall back to the realm's built-ins. Unknown names throw a ReferenceError, also
105
+ // under `typeof` (except PROBED_GLOBALS).
106
+ const createSharedCompileScope = (globals) => {
107
+ getSharedVmContext();
108
+ const realm = sharedRealmGlobal;
109
+ const vars = Object.assign(Object.create(null), globals);
110
+ // Built-ins this compile deleted from its global
111
+ const deleted = new Set();
112
+ const builtIn = (key) => key !== SHARED_SCOPE_GETTER && !deleted.has(key) && key in realm;
113
+ const read = (key) => {
114
+ if (key in vars) {
115
+ return vars[key];
116
+ }
117
+ if (builtIn(key)) {
118
+ return realm[key];
119
+ }
120
+ if (typeof key !== 'string' || PROBED_GLOBALS.has(key)) {
121
+ return undefined;
122
+ }
123
+ throw new realm.ReferenceError(`${key} is not defined`);
124
+ };
125
+ const remove = (key) => {
126
+ const removed = Reflect.deleteProperty(vars, key);
127
+ if (removed && builtIn(key)) {
128
+ deleted.add(key);
129
+ }
130
+ return removed;
131
+ };
132
+ // The compile's `globalThis` and top-level `this`: its own properties are `vars` (the target, so
133
+ // defining, deleting and reflecting on them behave as on an ordinary object), then the built-ins
134
+ const realmOwn = (key) => builtIn(key) && Object.prototype.hasOwnProperty.call(realm, key);
135
+ const compileGlobal = new Proxy(vars, {
136
+ get: (t, key) => (key in t || builtIn(key) ? read(key) : undefined),
137
+ has: (t, key) => key in t || builtIn(key),
138
+ // Without it, OrdinarySet would copy the built-in's attributes through getOwnPropertyDescriptor
139
+ set: (t, key, value) => {
140
+ t[key] = value;
141
+ deleted.delete(key);
142
+ return true;
143
+ },
144
+ defineProperty: (t, key, descriptor) => {
145
+ // Redefining a built-in keeps the attributes the descriptor leaves out, as on a global
146
+ const base = !Object.prototype.hasOwnProperty.call(t, key) && realmOwn(key)
147
+ ? { ...Reflect.getOwnPropertyDescriptor(realm, key), configurable: true }
148
+ : {};
149
+ const accessor = 'get' in descriptor || 'set' in descriptor;
150
+ const merged = accessor || 'get' in base
151
+ ? { enumerable: base.enumerable, configurable: base.configurable, ...descriptor }
152
+ : { ...base, ...descriptor };
153
+ deleted.delete(key);
154
+ return Reflect.defineProperty(t, key, merged);
155
+ },
156
+ deleteProperty: (_t, key) => remove(key),
157
+ ownKeys: (t) => [...new Set([
158
+ ...Reflect.ownKeys(t),
159
+ ...Reflect.ownKeys(realm).filter((key) => realmOwn(key)),
160
+ ])],
161
+ getOwnPropertyDescriptor: (t, key) => {
162
+ if (Object.prototype.hasOwnProperty.call(t, key) || !realmOwn(key)) {
163
+ return Reflect.getOwnPropertyDescriptor(t, key);
164
+ }
165
+ // A built-in the compile hasn't shadowed: reported configurable, as the target lacks it
166
+ return { ...Reflect.getOwnPropertyDescriptor(realm, key), configurable: true };
167
+ },
168
+ });
169
+ Object.defineProperty(vars, 'globalThis', { value: compileGlobal, writable: true, configurable: true, enumerable: false });
170
+ const scopeTraps = {
171
+ get: (_t, key) => read(key),
172
+ set: (t, key, value) => {
173
+ t[key] = value;
174
+ deleted.delete(key);
175
+ return true;
176
+ },
177
+ deleteProperty: (_t, key) => remove(key),
178
+ };
179
+ const scope = new Proxy(vars, {
180
+ ...scopeTraps,
181
+ has: (_t, key) => typeof key === 'string' && key !== SHARED_SCOPE_GETTER,
182
+ });
183
+ const strictScope = new Proxy(vars, {
184
+ ...scopeTraps,
185
+ has: (t, key) => typeof key === 'string' && (key in t || builtIn(key)),
186
+ });
187
+ // `new Function(...)` / `Function(...)` compile their body in this compile's scope, as they would
188
+ // at the global scope of a realm of its own. The result is an ordinary function of the realm.
189
+ const RealmFunction = realm.Function;
190
+ const compileFunction = function Function(...args) {
191
+ const body = args.length > 0 ? String(args[args.length - 1]) : '';
192
+ const params = args.slice(0, -1).map(String).join(',');
193
+ // Parse params and body on their own first, so malformed input throws the realm's SyntaxError
194
+ // instead of closing the wrapper below early.
195
+ RealmFunction(...args);
196
+ const factory = RealmFunction('scope', `with (scope) { return function anonymous(${params}\n) {\n${body}\n}; }`);
197
+ return factory(hasUseStrictDirective(body) ? strictScope : scope);
198
+ };
199
+ compileFunction.prototype = RealmFunction.prototype;
200
+ Object.setPrototypeOf(compileFunction, RealmFunction.prototype);
201
+ Object.defineProperty(compileFunction, 'length', { value: 1 });
202
+ Object.defineProperty(vars, 'Function', { value: compileFunction, writable: true, configurable: true, enumerable: false });
203
+ return { scope, strictScope, vars };
204
+ };
22
205
  const NATIVE_IS_SUPPORTED = (0, shared_1.isNativeSupported)();
23
206
  const moduleFileCache = {};
24
207
  const JINJA_SYNTAX = /{%|%}|{{|}}/ig;
@@ -87,6 +270,9 @@ class DataSchemaCompiler {
87
270
  compiledYamlCache;
88
271
  compiledJinjaCache;
89
272
  compileV8ContextCache = null;
273
+ sharedVmContext;
274
+ // Flag on: per-compile `with` scope evaluated inside the shared realm.
275
+ compileScope = null;
90
276
  // FIXME: Is public only because of tests, should be private
91
277
  compilePromise;
92
278
  currentQuery;
@@ -121,6 +307,7 @@ class DataSchemaCompiler {
121
307
  this.compiledScriptCache = options.compiledScriptCache;
122
308
  this.compiledYamlCache = options.compiledYamlCache;
123
309
  this.compiledJinjaCache = options.compiledJinjaCache;
310
+ this.sharedVmContext = !!options.sharedVmContext;
124
311
  }
125
312
  compileObjects(compileServices, objects, errorsReport) {
126
313
  try {
@@ -251,6 +438,22 @@ class DataSchemaCompiler {
251
438
  let compiledFiles = {};
252
439
  let asyncModules = [];
253
440
  let transpiledFiles = [];
441
+ // Results of `memo` calls. Unlike the objects above it survives between the compile stages:
442
+ // the model is evaluated once per stage, and `memo` makes it do a costly call (an API request
443
+ // from an `asyncModule`, say) only once per compile.
444
+ const memoResults = new Map();
445
+ // Stage that last looked up each key MemoKeyTranspiler generated
446
+ const memoCallSiteStages = new Map();
447
+ let memoStage = -1;
448
+ // Model objects go to the current stage only, while a memoized function runs in the first stage
449
+ // that asks for its key: objects defined from there would be missing from all later stages.
450
+ const assertOutsideMemo = (globalName) => {
451
+ const memoCall = memoFnStorage.getStore();
452
+ if (memoCall !== undefined) {
453
+ throw new UserError_1.UserError(`${globalName}() can't be called from the function passed to ${memoCall}: ` +
454
+ 'return the data from memo() and define model objects outside of it');
455
+ }
456
+ };
254
457
  const cleanup = () => {
255
458
  cubes = [];
256
459
  exports = {};
@@ -259,12 +462,15 @@ class DataSchemaCompiler {
259
462
  compiledFiles = {};
260
463
  asyncModules = [];
261
464
  };
262
- this.compileV8ContextCache = vm_1.default.createContext({
465
+ const compileGlobals = {
263
466
  view: (name, cube) => {
264
467
  const file = ctxFileStorage.getStore();
265
468
  if (!file) {
266
469
  throw new Error('No file stored in context');
267
470
  }
471
+ if (cube) {
472
+ assertOutsideMemo('view');
473
+ }
268
474
  return !cube ?
269
475
  this.cubeFactory({ ...name, fileName: file.fileName, isView: true }) :
270
476
  cubes.push({ ...cube, name, fileName: file.fileName, isView: true });
@@ -274,6 +480,9 @@ class DataSchemaCompiler {
274
480
  if (!file) {
275
481
  throw new Error('No file stored in context');
276
482
  }
483
+ if (cube) {
484
+ assertOutsideMemo('cube');
485
+ }
277
486
  return !cube ?
278
487
  this.cubeFactory({ ...name, fileName: file.fileName }) :
279
488
  cubes.push({ ...cube, name, fileName: file.fileName });
@@ -283,6 +492,7 @@ class DataSchemaCompiler {
283
492
  if (!file) {
284
493
  throw new Error('No file stored in context');
285
494
  }
495
+ assertOutsideMemo('context');
286
496
  return contexts.push({ ...context, name, fileName: file.fileName });
287
497
  },
288
498
  view_group: (name, viewGroup) => {
@@ -290,8 +500,14 @@ class DataSchemaCompiler {
290
500
  if (!file) {
291
501
  throw new Error('No file stored in context');
292
502
  }
503
+ assertOutsideMemo('view_group');
293
504
  viewGroups.push({ ...viewGroup, name, fileName: file.fileName });
294
- this.compileV8ContextCache[name] = name;
505
+ if (this.sharedVmContext) {
506
+ this.compileScope.vars[name] = name;
507
+ }
508
+ else {
509
+ this.compileV8ContextCache[name] = name;
510
+ }
295
511
  return name;
296
512
  },
297
513
  addExport: (obj) => {
@@ -314,11 +530,65 @@ class DataSchemaCompiler {
314
530
  if (!file) {
315
531
  throw new Error('No file stored in context');
316
532
  }
533
+ assertOutsideMemo('asyncModule');
317
534
  // We need to run async module code in the context of the original data model file
318
535
  // where it was defined. So we pass the same file to the async context.
319
536
  // @see https://nodejs.org/api/async_context.html#class-asynclocalstorage
320
537
  asyncModules.push(async () => ctxFileStorage.run(file, () => fn()));
321
538
  },
539
+ memo: (key, fn) => {
540
+ if (typeof key === 'function' && fn === undefined) {
541
+ // MemoKeyTranspiler gives `memo(fn)` its key: this call didn't go through it
542
+ throw new Error('memo() expects a key as its first argument: memo(key, fn)');
543
+ }
544
+ if (typeof fn !== 'function') {
545
+ throw new Error('memo() expects a function as its second argument');
546
+ }
547
+ // Strings are encoded too, or `memo('1', ...)` would share the result of `memo(1, ...)`
548
+ let cacheKey;
549
+ try {
550
+ cacheKey = JSON.stringify(key, memoKeyReplacer);
551
+ }
552
+ catch {
553
+ // BigInt or a cyclic structure
554
+ }
555
+ if (typeof cacheKey !== 'string') {
556
+ throw new Error('memo() expects a string or a JSON-serializable key as its first argument');
557
+ }
558
+ const callSite = transpilers_1.MemoKeyTranspiler.callSiteOf(key);
559
+ // For errors: the key as the code would write it
560
+ const label = callSite !== undefined ? `memo() at ${callSite}` : `memo(${cacheKey})`;
561
+ if (memoStage < 0 || !ctxFileStorage.getStore()) {
562
+ // Called from a member (`sql: () => memo(...)`) evaluated by a compiler or a query, not
563
+ // from a model file: it may run any number of times, so there's no stage to cache for
564
+ return memoFnStorage.run(label, fn);
565
+ }
566
+ if (callSite !== undefined) {
567
+ // A top-level call site runs once per stage: twice means it's in a function or a loop,
568
+ // where the one key would give every call the result of the first one
569
+ if (memoCallSiteStages.get(cacheKey) === memoStage) {
570
+ throw new UserError_1.UserError(`memo() at ${callSite} is called more than once per compile stage: as it's in a function or a loop, ` +
571
+ 'its result may depend on the arguments: pass a key that does, e.g. memo([\'table\', name], fn)');
572
+ }
573
+ memoCallSiteStages.set(cacheKey, memoStage);
574
+ }
575
+ let result = memoResults.get(cacheKey);
576
+ if (!result) {
577
+ // A promise is stored as is, so concurrent and later calls share one invocation.
578
+ // A throw is stored too: every stage has to see the same outcome.
579
+ try {
580
+ result = { value: memoFnStorage.run(label, fn) };
581
+ }
582
+ catch (error) {
583
+ result = { error };
584
+ }
585
+ memoResults.set(cacheKey, result);
586
+ }
587
+ if ('error' in result) {
588
+ throw result.error;
589
+ }
590
+ return result.value;
591
+ },
322
592
  require: (extensionName) => {
323
593
  const file = ctxFileStorage.getStore();
324
594
  if (!file) {
@@ -346,10 +616,17 @@ class DataSchemaCompiler {
346
616
  }
347
617
  },
348
618
  COMPILE_CONTEXT: this.standalone ? this.standaloneCompileContextProxy() : this.cloneCompileContextWithGetterAlias(this.compileContext || {}),
349
- });
619
+ };
620
+ if (this.sharedVmContext) {
621
+ this.compileScope = createSharedCompileScope(compileGlobals);
622
+ }
623
+ else {
624
+ this.compileV8ContextCache = vm_1.default.createContext(compileGlobals);
625
+ }
350
626
  const compilePhaseFirst = async (compilers, stage) => {
351
627
  // clear the objects for the next phase
352
628
  cleanup();
629
+ memoStage = stage;
353
630
  transpiledFiles = await transpilePhaseFirst(stage);
354
631
  // We render jinja and transpile yaml only once on first phase and then use resulting JS for these files
355
632
  // afterward avoiding costly YAML/Python parsing again. Original JS files are preserved as is for cache hits.
@@ -360,10 +637,11 @@ class DataSchemaCompiler {
360
637
  const compilePhase = async (compilers, stage) => {
361
638
  // clear the objects for the next phase
362
639
  cleanup();
640
+ memoStage = stage;
363
641
  transpiledFiles = await transpilePhase(stage);
364
642
  return this.compileCubeFiles(cubes, contexts, viewGroups, compiledFiles, asyncModules, compilers, transpiledFiles, errorsReport);
365
643
  };
366
- return compilePhaseFirst({ cubeCompilers: this.cubeNameCompilers }, 0)
644
+ const compileAll = () => compilePhaseFirst({ cubeCompilers: this.cubeNameCompilers }, 0)
367
645
  .then(() => compilePhase({ cubeCompilers: this.preTranspileCubeCompilers.concat([this.viewCompilationGate]) }, 1))
368
646
  .then(() => (this.viewCompilationGate.shouldCompileViews() ?
369
647
  compilePhase({ cubeCompilers: this.viewCompilers }, 2)
@@ -379,6 +657,12 @@ class DataSchemaCompiler {
379
657
  cleanup();
380
658
  transpiledFiles = [];
381
659
  toCompile = [];
660
+ // The functions put into the compile context (cube, view, require, ...) close over
661
+ // this scope, and member functions keep that context alive as long as the compiled
662
+ // model lives: drop the model files' contents here, or they stay retained with it.
663
+ originalJsFiles.length = 0;
664
+ jinjaTemplatedFiles.length = 0;
665
+ yamlFiles.length = 0;
382
666
  if (transpilationNative) {
383
667
  // Clean up cache
384
668
  const dummyFile = {
@@ -390,7 +674,18 @@ class DataSchemaCompiler {
390
674
  else if (this.workerPool) {
391
675
  this.workerPool.terminate();
392
676
  }
677
+ })
678
+ // A failed compile mustn't keep the fetched data either
679
+ .finally(() => {
680
+ memoStage = -1;
681
+ memoResults.clear();
682
+ memoCallSiteStages.clear();
683
+ // These hold the globals.py TemplateContext, where the Python @memo caches live
684
+ this.pythonContext = null;
685
+ this.yamlCompiler.free();
393
686
  });
687
+ // Every continuation of the compile (asyncModule callbacks included) sees its scope
688
+ return this.compileScope ? sharedScopeStorage.run(this.compileScope, compileAll) : compileAll();
394
689
  }
395
690
  compile() {
396
691
  if (!this.compilePromise) {
@@ -400,6 +695,7 @@ class DataSchemaCompiler {
400
695
  }
401
696
  // Free unneeded resources
402
697
  this.compileV8ContextCache = null;
698
+ this.compileScope = null;
403
699
  this.cubeDictionary.free();
404
700
  this.cubeOnlySymbols.free();
405
701
  this.cubeAndViewSymbols.free();
@@ -625,6 +921,8 @@ class DataSchemaCompiler {
625
921
  return undefined;
626
922
  }
627
923
  async transpileYamlFile(file, errorsReport, { cubeNames, cubeSymbols, compilerId, jinjaUsed }) {
924
+ // The YAML to JS output only depends on the content: YAML is transpiled once, in the
925
+ // first phase, before any cube symbols are known. So the key is safe to share between apps.
628
926
  const cacheKey = crypto_1.default.createHash('md5').update(file.content).digest('hex');
629
927
  if (this.compiledYamlCache.has(cacheKey)) {
630
928
  const content = this.compiledYamlCache.get(cacheKey);
@@ -643,7 +941,10 @@ class DataSchemaCompiler {
643
941
  errorsReport.addErrors(res[0].errors, file.fileName);
644
942
  errorsReport.addWarnings(res[0].warnings);
645
943
  errorsReport.exitFile();
646
- this.compiledYamlCache.set(cacheKey, res[0].code);
944
+ // A hit doesn't replay errors, so a failed transpilation must not be cached
945
+ if (!res[0].errors?.length) {
946
+ this.compiledYamlCache.set(cacheKey, res[0].code);
947
+ }
647
948
  return { ...file, content: res[0].code };
648
949
  }
649
950
  else {
@@ -658,7 +959,9 @@ class DataSchemaCompiler {
658
959
  const res = await this.workerPool.exec('transpileYaml', [data]);
659
960
  errorsReport.addErrors(res.errors, file.fileName);
660
961
  errorsReport.addWarnings(res.warnings);
661
- this.compiledYamlCache.set(cacheKey, res.content);
962
+ if (!res.errors?.length) {
963
+ this.compiledYamlCache.set(cacheKey, res.content);
964
+ }
662
965
  return { ...file, content: res.content };
663
966
  }
664
967
  }
@@ -719,7 +1022,12 @@ class DataSchemaCompiler {
719
1022
  this.compileJsFile(file, errorsReport, { doSyntaxCheck });
720
1023
  }
721
1024
  getJsScript(file) {
722
- const cacheKey = crypto_1.default.createHash('md5').update(file.content).digest('hex');
1025
+ // The script carries its file name (stack traces), and the cache may be shared between apps
1026
+ const cacheKey = crypto_1.default.createHash('md5')
1027
+ .update(file.fileName)
1028
+ .update('\0')
1029
+ .update(file.content)
1030
+ .digest('hex');
723
1031
  if (this.compiledScriptCache.has(cacheKey)) {
724
1032
  return this.compiledScriptCache.get(cacheKey);
725
1033
  }
@@ -727,6 +1035,25 @@ class DataSchemaCompiler {
727
1035
  this.compiledScriptCache.set(cacheKey, script);
728
1036
  return script;
729
1037
  }
1038
+ getSharedJsScript(file) {
1039
+ // Not the source getJsScript compiles (the wrapper), hence the prefix
1040
+ const cacheKey = `shared:${crypto_1.default.createHash('md5')
1041
+ .update(file.fileName)
1042
+ .update('\0')
1043
+ .update(file.content)
1044
+ .digest('hex')}`;
1045
+ if (this.compiledScriptCache.has(cacheKey)) {
1046
+ return this.compiledScriptCache.get(cacheKey);
1047
+ }
1048
+ // The prefix is on line 1, so a negative column offset keeps positions in stack traces intact.
1049
+ const prefix = sharedScriptPrefix(hasUseStrictDirective(file.content));
1050
+ const script = new vm_1.default.Script(`${prefix}${file.content}${SHARED_SCRIPT_SUFFIX}`, {
1051
+ filename: file.fileName,
1052
+ columnOffset: -prefix.length,
1053
+ });
1054
+ this.compiledScriptCache.set(cacheKey, script);
1055
+ return script;
1056
+ }
730
1057
  compileJsFile(file, errorsReport, { doSyntaxCheck } = { doSyntaxCheck: false }) {
731
1058
  if (doSyntaxCheck) {
732
1059
  // There is no need to run syntax check for data model files
@@ -738,6 +1065,16 @@ class DataSchemaCompiler {
738
1065
  }
739
1066
  }
740
1067
  try {
1068
+ if (this.sharedVmContext) {
1069
+ const script = this.getSharedJsScript(file);
1070
+ const scope = this.compileScope;
1071
+ ctxFileStorage.run(file, () => {
1072
+ sharedScopeStorage.run(scope, () => {
1073
+ script.runInContext(getSharedVmContext(), { timeout: 15000 });
1074
+ });
1075
+ });
1076
+ return;
1077
+ }
741
1078
  const script = this.getJsScript(file);
742
1079
  // We use AsyncLocalStorage to store the current file context
743
1080
  // so that it can be accessed in the script execution context even within async functions.