Skip to content

Commit d0b9013

Browse files
author
Robert Jackson
authored
Expose option to allow a new sandbox per visit (#252)
Expose option to allow a new sandbox per visit
2 parents 1d0e4c8 + 3b28a2e commit d0b9013

File tree

4 files changed

+40
-12
lines changed

4 files changed

+40
-12
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ configuration:
6868
- `shouldRender`: boolean to indicate whether the app should do rendering or not. If set to false, it puts the app in routing-only. Defaults to true.
6969
- `disableShoebox`: boolean to indicate whether we should send the API data in the shoebox. If set to false, it will not send the API data used for rendering the app on server side in the index.html. Defaults to false.
7070
- `destroyAppInstanceInMs`: whether to destroy the instance in the given number of ms. This is a failure mechanism to not wedge the Node process
71+
- `buildSandboxPerVisit`: whether to create a new sandbox context per-visit (slows down each visit, but guarantees no prototype leakages can occur), or reuse the existing sandbox (faster per-request, but each request shares the same set of prototypes). Defaults to false.
7172

7273
### Build Your App
7374

src/ember-app.js

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,9 @@ class EmberApp {
183183
* Perform any cleanup that is needed
184184
*/
185185
destroy() {
186-
// TODO: expose as public api (through the top level) so that we can
187-
// cleanup pre-warmed visits
186+
if (this._applicationInstance) {
187+
this._applicationInstance.destroy();
188+
}
188189
}
189190

190191
/**
@@ -242,12 +243,27 @@ class EmberApp {
242243
* @param {Object} bootOptions An object containing the boot options that are used by
243244
* by ember to decide whether it needs to do rendering or not.
244245
* @param {Object} result
246+
* @param {Boolean} buildSandboxPerVisit if true, a new sandbox will
247+
* **always** be created, otherwise one
248+
* is created for the first request
249+
* only
245250
* @return {Promise<instance>} instance
246251
*/
247-
async visitRoute(path, fastbootInfo, bootOptions, result) {
248-
let app = await this.buildApp();
249-
result.applicationInstance = app;
250-
252+
async _visit(path, fastbootInfo, bootOptions, result, buildSandboxPerVisit) {
253+
let shouldBuildApp = buildSandboxPerVisit || this._applicationInstance === undefined;
254+
255+
let app = shouldBuildApp ? await this.buildApp() : this._applicationInstance;
256+
257+
if (buildSandboxPerVisit) {
258+
// entangle the specific application instance to the result, so it can be
259+
// destroyed when result._destroy() is called (after the visit is
260+
// completed)
261+
result.applicationInstance = app;
262+
} else {
263+
// save the created application instance so that we can clean it up when
264+
// this instance of `src/ember-app.js` is destroyed (e.g. reload)
265+
this._applicationInstance = app;
266+
}
251267
await app.boot();
252268

253269
let instance = await app.buildInstance();
@@ -278,6 +294,7 @@ class EmberApp {
278294
* @param {Boolean} [options.shouldRender] whether the app should do rendering or not. If set to false, it puts the app in routing-only.
279295
* @param {Boolean} [options.disableShoebox] whether we should send the API data in the shoebox. If set to false, it will not send the API data used for rendering the app on server side in the index.html.
280296
* @param {Integer} [options.destroyAppInstanceInMs] whether to destroy the instance in the given number of ms. This is a failure mechanism to not wedge the Node process (See: https://github.com/ember-fastboot/fastboot/issues/90)
297+
* @param {Boolean} [options.buildSandboxPerVisit] whether to create a new sandbox context per-visit, or reuse the existing sandbox
281298
* @param {ClientRequest}
282299
* @param {ClientResponse}
283300
* @returns {Promise<Result>} result
@@ -314,7 +331,13 @@ class EmberApp {
314331
}
315332

316333
try {
317-
await this.visitRoute(path, fastbootInfo, bootOptions, result);
334+
await this._visit(
335+
path,
336+
fastbootInfo,
337+
bootOptions,
338+
result,
339+
options.buildSandboxPerVisit === true
340+
);
318341

319342
if (!disableShoebox) {
320343
// if shoebox is not disabled, then create the shoebox and send API data

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class FastBoot {
7575
* @param {Boolean} [options.shouldRender] whether the app should do rendering or not. If set to false, it puts the app in routing-only.
7676
* @param {Boolean} [options.disableShoebox] whether we should send the API data in the shoebox. If set to false, it will not send the API data used for rendering the app on server side in the index.html.
7777
* @param {Integer} [options.destroyAppInstanceInMs] whether to destroy the instance in the given number of ms. This is a failure mechanism to not wedge the Node process (See: https://github.com/ember-fastboot/fastboot/issues/90)
78+
* @param {Boolean} [options.buildSandboxPerVisit=false] whether to create a new sandbox context per-visit (slows down each visit, but guarantees no prototype leakages can occur), or reuse the existing sandbox (faster per-request, but each request shares the same set of prototypes)
7879
* @returns {Promise<Result>} result
7980
*/
8081
async visit(path, options = {}) {

test/fastboot-test.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -401,45 +401,48 @@ describe('FastBoot', function() {
401401
});
402402
});
403403

404-
it('in app prototype mutations do not leak across visits', async function() {
404+
it('in app prototype mutations do not leak across visits with buildSandboxPerVisit=true', async function() {
405405
this.timeout(3000);
406406

407407
var fastboot = new FastBoot({
408408
distPath: fixture('app-with-prototype-mutations'),
409409
});
410410

411-
let result = await fastboot.visit('/');
411+
let result = await fastboot.visit('/', { buildSandboxPerVisit: true });
412412
let html = await result.html();
413413

414414
expect(html).to.match(/Items: 1/);
415415

416-
result = await fastboot.visit('/');
416+
result = await fastboot.visit('/', { buildSandboxPerVisit: true });
417417
html = await result.html();
418418

419419
expect(html).to.match(/Items: 1/);
420420

421-
result = await fastboot.visit('/');
421+
result = await fastboot.visit('/', { buildSandboxPerVisit: true });
422422
html = await result.html();
423423

424424
expect(html).to.match(/Items: 1/);
425425
});
426426

427-
it('errors can be properly attributed', async function() {
427+
it('errors can be properly attributed with buildSandboxPerVisit=true', async function() {
428428
this.timeout(3000);
429429

430430
var fastboot = new FastBoot({
431431
distPath: fixture('onerror-per-visit'),
432432
});
433433

434434
let first = fastboot.visit('/slow/100/reject', {
435+
buildSandboxPerVisit: true,
435436
request: { url: '/slow/100/reject', headers: {} },
436437
});
437438

438439
let second = fastboot.visit('/slow/50/resolve', {
440+
buildSandboxPerVisit: true,
439441
request: { url: '/slow/50/resolve', headers: {} },
440442
});
441443

442444
let third = fastboot.visit('/slow/25/resolve', {
445+
buildSandboxPerVisit: true,
443446
request: { url: '/slow/25/resolve', headers: {} },
444447
});
445448

0 commit comments

Comments
 (0)