@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.
- package/dist/src/adapter/BaseQuery.d.ts.map +1 -1
- package/dist/src/adapter/BaseQuery.js +2 -0
- package/dist/src/adapter/BaseQuery.js.map +1 -1
- package/dist/src/adapter/MssqlQuery.d.ts.map +1 -1
- package/dist/src/adapter/MssqlQuery.js +6 -0
- package/dist/src/adapter/MssqlQuery.js.map +1 -1
- package/dist/src/compiler/CubeEvaluator.d.ts +9 -1
- package/dist/src/compiler/CubeEvaluator.d.ts.map +1 -1
- package/dist/src/compiler/CubeEvaluator.js +19 -1
- package/dist/src/compiler/CubeEvaluator.js.map +1 -1
- package/dist/src/compiler/CubeSymbols.d.ts.map +1 -1
- package/dist/src/compiler/CubeSymbols.js +4 -0
- package/dist/src/compiler/CubeSymbols.js.map +1 -1
- package/dist/src/compiler/CubeToMetaTransformer.d.ts +4 -1
- package/dist/src/compiler/CubeToMetaTransformer.d.ts.map +1 -1
- package/dist/src/compiler/CubeToMetaTransformer.js +7 -1
- package/dist/src/compiler/CubeToMetaTransformer.js.map +1 -1
- package/dist/src/compiler/CubeValidator.d.ts.map +1 -1
- package/dist/src/compiler/CubeValidator.js +6 -0
- package/dist/src/compiler/CubeValidator.js.map +1 -1
- package/dist/src/compiler/DataSchemaCompiler.d.ts +7 -0
- package/dist/src/compiler/DataSchemaCompiler.d.ts.map +1 -1
- package/dist/src/compiler/DataSchemaCompiler.js +344 -7
- package/dist/src/compiler/DataSchemaCompiler.js.map +1 -1
- package/dist/src/compiler/PrepareCompiler.d.ts +6 -0
- package/dist/src/compiler/PrepareCompiler.d.ts.map +1 -1
- package/dist/src/compiler/PrepareCompiler.js +8 -3
- package/dist/src/compiler/PrepareCompiler.js.map +1 -1
- package/dist/src/compiler/StringInterning.d.ts +16 -0
- package/dist/src/compiler/StringInterning.d.ts.map +1 -0
- package/dist/src/compiler/StringInterning.js +106 -0
- package/dist/src/compiler/StringInterning.js.map +1 -0
- package/dist/src/compiler/index.d.ts +1 -0
- package/dist/src/compiler/index.d.ts.map +1 -1
- package/dist/src/compiler/index.js +5 -0
- package/dist/src/compiler/index.js.map +1 -1
- package/dist/src/compiler/transpilers/IIFETranspiler.d.ts.map +1 -1
- package/dist/src/compiler/transpilers/IIFETranspiler.js +8 -2
- package/dist/src/compiler/transpilers/IIFETranspiler.js.map +1 -1
- package/dist/src/compiler/transpilers/MemoKeyTranspiler.d.ts +11 -0
- package/dist/src/compiler/transpilers/MemoKeyTranspiler.d.ts.map +1 -0
- package/dist/src/compiler/transpilers/MemoKeyTranspiler.js +72 -0
- package/dist/src/compiler/transpilers/MemoKeyTranspiler.js.map +1 -0
- package/dist/src/compiler/transpilers/index.d.ts +1 -0
- package/dist/src/compiler/transpilers/index.d.ts.map +1 -1
- package/dist/src/compiler/transpilers/index.js +1 -0
- package/dist/src/compiler/transpilers/index.js.map +1 -1
- package/dist/src/compiler/transpilers/transpiler_worker.js +2 -0
- package/dist/src/compiler/transpilers/transpiler_worker.js.map +1 -1
- 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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|