Skip to content

Commit cf0f451

Browse files
author
Robert Jackson
authored
Refactor to use a single sandboxed context per visit request. (#236)
Refactor to use a single sandboxed context per visit request.
2 parents 5ad17de + 53ee52e commit cf0f451

29 files changed

Lines changed: 132992 additions & 283 deletions

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ let app = new FastBoot({
2828
distPath: 'path/to/dist',
2929
// optional boolean flag when set to true does not reject the promise if there are rendering errors (defaults to false)
3030
resilient: <boolean>,
31-
sandbox: 'path/to/sandbox/class', // optional sandbox class (defaults to vm-sandbox)
3231
sandboxGlobals: {...} // optional map of key value pairs to expose in the sandbox
3332
});
3433

src/ember-app.js

Lines changed: 104 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const fs = require('fs');
4+
const vm = require('vm');
45
const path = require('path');
56
const chalk = require('chalk');
67

@@ -9,6 +10,7 @@ const SimpleDOM = require('simple-dom');
910
const resolve = require('resolve');
1011
const debug = require('debug')('fastboot:ember-app');
1112

13+
const Sandbox = require('./sandbox');
1214
const FastBootInfo = require('./fastboot-info');
1315
const Result = require('./result');
1416
const FastBootSchemaVersions = require('./fastboot-schema-versions');
@@ -27,15 +29,15 @@ class EmberApp {
2729
* Create a new EmberApp.
2830
* @param {Object} options
2931
* @param {string} options.distPath - path to the built Ember application
30-
* @param {Sandbox} [options.sandbox=VMSandbox] - sandbox to use
3132
* @param {Object} [options.sandboxGlobals] - sandbox variables that can be added or used for overrides in the sandbox.
3233
*/
3334
constructor(options) {
34-
let distPath = path.resolve(options.distPath);
35+
// TODO: make these two into builder functions
36+
this.sandboxGlobals = options.sandboxGlobals;
37+
38+
let distPath = (this.distPath = path.resolve(options.distPath));
3539
let config = this.readPackageJSON(distPath);
3640

37-
this.appFilePaths = config.appFiles;
38-
this.vendorFilePaths = config.vendorFiles;
3941
this.moduleWhitelist = config.moduleWhitelist;
4042
this.hostWhitelist = config.hostWhitelist;
4143
this.config = config.config;
@@ -57,23 +59,25 @@ class EmberApp {
5759

5860
this.html = fs.readFileSync(config.htmlFile, 'utf8');
5961

60-
this.sandbox = this.buildSandbox(distPath, options.sandbox, options.sandboxGlobals);
61-
this.app = this.retrieveSandboxedApp();
62+
this.sandboxRequire = this.buildWhitelistedRequire(this.moduleWhitelist, distPath);
63+
let filePaths = [require.resolve('./scripts/install-source-map-support')].concat(
64+
config.vendorFiles,
65+
config.appFiles
66+
);
67+
this.scripts = buildScripts(filePaths);
68+
69+
// Ensure that the dist files can be evaluated and the `Ember.Application`
70+
// instance created.
71+
this.buildApp();
6272
}
6373

6474
/**
6575
* @private
6676
*
6777
* Builds and initializes a new sandbox to run the Ember application in.
68-
*
69-
* @param {string} distPath path to the built Ember app to load
70-
* @param {Sandbox} [sandboxClass=VMSandbox] sandbox class to use
71-
* @param {Object} [sandboxGlobals={}] any additional variables to expose in the sandbox or override existing in the sandbox
7278
*/
73-
buildSandbox(distPath, sandboxClass, sandboxGlobals) {
74-
const { config, appName } = this;
75-
76-
let sandboxRequire = this.buildWhitelistedRequire(this.moduleWhitelist, distPath);
79+
buildSandbox() {
80+
const { distPath, sandboxGlobals, config, appName, sandboxRequire } = this;
7781

7882
function fastbootConfig(key) {
7983
if (!key) {
@@ -89,21 +93,22 @@ class EmberApp {
8993
}
9094

9195
// add any additional user provided variables or override the default globals in the sandbox
92-
let globals = {
93-
najax,
94-
FastBoot: {
95-
require: sandboxRequire,
96-
config: fastbootConfig,
97-
98-
get distPath() {
99-
return distPath;
96+
let globals = Object.assign(
97+
{
98+
najax,
99+
FastBoot: {
100+
require: sandboxRequire,
101+
config: fastbootConfig,
102+
103+
get distPath() {
104+
return distPath;
105+
},
100106
},
101107
},
102-
};
103-
104-
globals = Object.assign(globals, sandboxGlobals);
108+
sandboxGlobals
109+
);
105110

106-
return new sandboxClass({ globals });
111+
return new Sandbox(globals);
107112
}
108113

109114
/**
@@ -180,43 +185,31 @@ class EmberApp {
180185
}
181186

182187
/**
183-
* @private
184-
*
185-
* Loads the app and vendor files in the sandbox (Node vm).
186-
*
188+
* Perform any cleanup that is needed
187189
*/
188-
loadAppFiles() {
189-
let sandbox = this.sandbox;
190-
let appFilePaths = this.appFilePaths;
191-
let vendorFilePaths = this.vendorFilePaths;
192-
193-
sandbox.eval('sourceMapSupport.install(Error);');
194-
195-
debug('evaluating app and vendor files');
196-
197-
vendorFilePaths.forEach(function(vendorFilePath) {
198-
debug('evaluating vendor file %s', vendorFilePath);
199-
let vendorFile = fs.readFileSync(vendorFilePath, 'utf8');
200-
sandbox.eval(vendorFile, vendorFilePath);
201-
});
202-
debug('vendor file evaluated');
203-
204-
appFilePaths.forEach(function(appFilePath) {
205-
debug('evaluating app file %s', appFilePath);
206-
let appFile = fs.readFileSync(appFilePath, 'utf8');
207-
sandbox.eval(appFile, appFilePath);
208-
});
209-
debug('app files evaluated');
190+
destroy() {
191+
// TODO: expose as public api (through the top level) so that we can
192+
// cleanup pre-warmed visits
210193
}
211194

212195
/**
213196
* @private
214197
*
215-
* Create the ember application in the sandbox.
198+
* Creates a new `Application`
216199
*
200+
* @returns {Ember.Application} instance
217201
*/
218-
createEmberApp() {
219-
let sandbox = this.sandbox;
202+
buildApp() {
203+
let sandbox = this.buildSandbox();
204+
205+
debug('adding files to sandbox');
206+
207+
for (let script of this.scripts) {
208+
debug('evaluating file %s', script);
209+
sandbox.runScript(script);
210+
}
211+
212+
debug('files evaluated');
220213

221214
// Retrieve the application factory from within the sandbox
222215
let AppFactory = sandbox.run(function(ctx) {
@@ -230,48 +223,12 @@ class EmberApp {
230223
);
231224
}
232225

233-
// Otherwise, return a new `Ember.Application` instance
234-
return AppFactory['default']();
235-
}
236-
237-
/**
238-
* @private
239-
*
240-
* Initializes the sandbox by evaluating the Ember app's JavaScript
241-
* code, then retrieves the application factory from the sandbox and creates a new
242-
* `Ember.Application`.
243-
*
244-
* @returns {Ember.Application} the Ember application from the sandbox
245-
*/
246-
retrieveSandboxedApp() {
247-
this.loadAppFiles();
248-
249-
return this.createEmberApp();
250-
}
251-
252-
/**
253-
* Destroys the app and its sandbox.
254-
*/
255-
destroy() {
256-
if (this.app) {
257-
this.app.destroy();
258-
}
226+
debug('creating application');
259227

260-
this.sandbox = null;
261-
}
228+
// Otherwise, return a new `Ember.Application` instance
229+
let app = AppFactory['default']();
262230

263-
/**
264-
* @private
265-
*
266-
* Creates a new `ApplicationInstance` from the sandboxed `Application`.
267-
*
268-
* @returns {Promise<Ember.ApplicationInstance>} instance
269-
*/
270-
buildAppInstance() {
271-
return this.app.boot().then(function(app) {
272-
debug('building instance');
273-
return app.buildInstance();
274-
});
231+
return app;
275232
}
276233

277234
/**
@@ -292,20 +249,20 @@ class EmberApp {
292249
* @param {Object} result
293250
* @return {Promise<instance>} instance
294251
*/
295-
visitRoute(path, fastbootInfo, bootOptions, result) {
296-
let instance;
297-
298-
return this.buildAppInstance()
299-
.then(appInstance => {
300-
instance = appInstance;
301-
result.instance = instance;
302-
registerFastBootInfo(fastbootInfo, instance);
303-
304-
return instance.boot(bootOptions);
305-
})
306-
.then(() => instance.visit(path, bootOptions))
307-
.then(() => fastbootInfo.deferredPromise)
308-
.then(() => instance);
252+
async visitRoute(path, fastbootInfo, bootOptions, result) {
253+
let app = await this.buildApp();
254+
result.applicationInstance = app;
255+
256+
await app.boot();
257+
258+
let instance = await app.buildInstance();
259+
result.applicationInstanceInstance = instance;
260+
261+
registerFastBootInfo(fastbootInfo, instance);
262+
263+
await instance.boot(bootOptions);
264+
await instance.visit(path, bootOptions);
265+
await fastbootInfo.deferredPromise;
309266
}
310267

311268
/**
@@ -330,7 +287,7 @@ class EmberApp {
330287
* @param {ClientResponse}
331288
* @returns {Promise<Result>} result
332289
*/
333-
visit(path, options) {
290+
async visit(path, options) {
334291
let req = options.request;
335292
let res = options.response;
336293
let html = options.html || this.html;
@@ -345,42 +302,44 @@ class EmberApp {
345302
});
346303

347304
let doc = bootOptions.document;
305+
let result = new Result(doc, html, fastbootInfo);
348306

349-
let result = new Result({
350-
doc: doc,
351-
html: html,
352-
fastbootInfo: fastbootInfo,
353-
});
354-
307+
// TODO: Use Promise.race here
355308
let destroyAppInstanceTimer;
356309
if (destroyAppInstanceInMs > 0) {
357310
// start a timer to destroy the appInstance forcefully in the given ms.
358311
// This is a failure mechanism so that node process doesn't get wedged if the `visit` never completes.
359312
destroyAppInstanceTimer = setTimeout(function() {
360-
if (result._destroyAppInstance()) {
313+
if (result._destroy()) {
361314
result.error = new Error(
362315
'App instance was forcefully destroyed in ' + destroyAppInstanceInMs + 'ms'
363316
);
364317
}
365318
}, destroyAppInstanceInMs);
366319
}
367320

368-
return this.visitRoute(path, fastbootInfo, bootOptions, result)
369-
.then(() => {
370-
if (!disableShoebox) {
371-
// if shoebox is not disabled, then create the shoebox and send API data
372-
createShoebox(doc, fastbootInfo);
373-
}
374-
})
375-
.catch(error => (result.error = error))
376-
.then(() => result._finalize())
377-
.finally(() => {
378-
if (result._destroyAppInstance()) {
379-
if (destroyAppInstanceTimer) {
380-
clearTimeout(destroyAppInstanceTimer);
381-
}
382-
}
383-
});
321+
try {
322+
await this.visitRoute(path, fastbootInfo, bootOptions, result);
323+
324+
if (!disableShoebox) {
325+
// if shoebox is not disabled, then create the shoebox and send API data
326+
createShoebox(doc, fastbootInfo);
327+
}
328+
329+
result._finalize();
330+
} catch (error) {
331+
// eslint-disable-next-line require-atomic-updates
332+
result.error = error;
333+
} finally {
334+
// ensure we invoke `Ember.Application.destroy()` and
335+
// `Ember.ApplicationInstance.destroy()`, but use `result._destroy()` so
336+
// that the `result` object's internal `this.isDestroyed` flag is correct
337+
result._destroy();
338+
339+
clearTimeout(destroyAppInstanceTimer);
340+
}
341+
342+
return result;
384343
}
385344

386345
/**
@@ -455,14 +414,14 @@ class EmberApp {
455414
});
456415

457416
return {
458-
appFiles: appFiles,
459-
vendorFiles: vendorFiles,
417+
appFiles,
418+
vendorFiles,
460419
htmlFile: path.join(distPath, manifest.htmlFile),
461420
moduleWhitelist: pkg.fastboot.moduleWhitelist,
462421
hostWhitelist: pkg.fastboot.hostWhitelist,
463-
config: config,
464-
appName: appName,
465-
schemaVersion: schemaVersion,
422+
config,
423+
appName,
424+
schemaVersion,
466425
};
467426
}
468427

@@ -553,4 +512,11 @@ function registerFastBootInfo(info, instance) {
553512
info.register(instance);
554513
}
555514

515+
function buildScripts(filePaths) {
516+
return filePaths.filter(Boolean).map(filePath => {
517+
let source = fs.readFileSync(filePath, { encoding: 'utf8' });
518+
519+
return new vm.Script(source, { filename: filePath });
520+
});
521+
}
556522
module.exports = EmberApp;

0 commit comments

Comments
 (0)