Skip to content

Commit

Permalink
Add support for top-level await
Browse files Browse the repository at this point in the history
  • Loading branch information
appurva21 committed May 2, 2024
1 parent 1990b3b commit d8318c1
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 25 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
unreleased:
new features:
- Added support for top-level await in scripts

4.7.1:
date: 2024-04-03
fixed bugs:
Expand Down
28 changes: 10 additions & 18 deletions lib/sandbox/execute-context.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
const { isNonLegacySandbox } = require('./non-legacy-codemarkers');
const _ = require('lodash'),
legacy = require('./postman-legacy-interface'),

NONLEGACY_SANDBOX_MARKERS = {
'"use sandbox2";': true,
'\'use sandbox2\';': true
};
legacy = require('./postman-legacy-interface');

module.exports = function (scope, code, execution, console, timers, pmapi, onAssertion, options) {
// if there is no code, then no point bubbling anything up
Expand All @@ -15,7 +11,7 @@ module.exports = function (scope, code, execution, console, timers, pmapi, onAss
// start by resetting the scope
scope.reset();

if (NONLEGACY_SANDBOX_MARKERS[code.substr(0, 15)] || options.disableLegacyAPIs) {
if (isNonLegacySandbox(code) || options.disableLegacyAPIs) {
// ensure any previously added global variables from legacy are torn down. side-effect is that if user
// explicitly created global variables with same name as legacy ones, they will be torn down too!
// for that reason, the setup function tags the scope and avoids tearing down an scope that was never setup
Expand Down Expand Up @@ -49,24 +45,20 @@ module.exports = function (scope, code, execution, console, timers, pmapi, onAss
clearImmediate: timers.clearImmediate
});

scope.exec(code, function (err) {
scope.exec(code, { async: true }, function (err) {
// we check if the execution went async by determining the timer queue length at this time
execution.return.async = (timers.queueLength() > 0);

// call this hook to perform any post script execution tasks
legacy.finish(scope, pmapi, onAssertion);

function complete () {
// if timers are running, we do not need to proceed with any logic of completing execution. instead we wait
// for timer completion callback to fire
if (execution.return.async) {
return err && timers.error(err); // but if we had error, we pass it to async error handler
}

// at this stage, the script is a synchronous script, we simply forward whatever has come our way
timers.terminate(err);
// if timers are running, we do not need to proceed with any logic of completing execution. instead we wait
// for timer completion callback to fire
if (execution.return.async) {
return err && timers.error(err); // but if we had error, we pass it to async error handler
}

timers.wrapped.setImmediate(complete);
// at this stage, the script is a synchronous script, we simply forward whatever has come our way
timers.terminate(err);
});
};
19 changes: 13 additions & 6 deletions lib/sandbox/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const _ = require('lodash'),
PostmanAPI = require('./pmapi'),
PostmanCookieStore = require('./cookie-store'),
createPostmanRequire = require('./pm-require'),
{ isNonLegacySandbox, getNonLegacyCodeMarker } = require('./non-legacy-codemarkers'),

EXECUTION_RESULT_EVENT_BASE = 'execution.result.',
EXECUTION_REQUEST_EVENT_BASE = 'execution.request.',
Expand Down Expand Up @@ -115,9 +116,14 @@ module.exports = function (bridge, glob) {
assertionEventName = EXECUTION_ASSERTION_EVENT_BASE + id,
skipRequestEventName = EXECUTION_SKIP_REQUEST_EVENT_BASE + id,

// extract the code from event. The event can be the code itself and we know that if the event is of type
// string.
code = _.isFunction(event.script && event.script.toSource) && event.script.toSource(),
// extract the code from event
code = _.isFunction(event.script && event.script.toSource) && ((code) => {
// wrap it in an async function to support top-level await
const asyncCode = `;(async()=>{;${code};})().then(__exitscope).catch(__exitscope);`;

return isNonLegacySandbox(code) ? `${getNonLegacyCodeMarker()}${asyncCode}` : asyncCode;
})(event.script.toSource()),

// create the execution object
execution = new Execution(id, event, context, { ...options, initializeExecution }),

Expand Down Expand Up @@ -160,8 +166,7 @@ module.exports = function (bridge, glob) {
// create the controlled timers
timers = new PostmanTimers(null, function (err) {
if (err) { // propagate the error out of sandbox
bridge.dispatch(errorEventName, options.cursor, err);
bridge.dispatch(EXECUTION_ERROR_EVENT, options.cursor, err);
onError(err);
}
}, function () {
execution.return.async = true;
Expand Down Expand Up @@ -223,7 +228,9 @@ module.exports = function (bridge, glob) {
// and one of them throws an error, this handler will be triggered
// for all of them. This is a limitation of uvm as there is no way
// to isolate the uncaught exception handling for each execution.
bridge.on('uncaughtException', onError);
bridge.on('uncaughtException', (err) => {
onError(err);
});

if (!options.resolvedPackages) {
disabledAPIs.push('require');
Expand Down
14 changes: 14 additions & 0 deletions lib/sandbox/non-legacy-codemarkers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const NONLEGACY_SANDBOX_MARKERS = {
'"use sandbox2";': true,
'\'use sandbox2\';': true
};

module.exports = {
isNonLegacySandbox (code) {
return NONLEGACY_SANDBOX_MARKERS[code.substr(0, 15)];
},

getNonLegacyCodeMarker () {
return Object.keys(NONLEGACY_SANDBOX_MARKERS)[0];
}
};
2 changes: 1 addition & 1 deletion lib/sandbox/timers.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ function Timerz (delegations, onError, onAnyTimerStart, onAllTimerEnd) {

function maybeEndAllTimers () {
if (pending === 0 && computeTimerEvents.started) {
!clearing && (typeof onAllTimerEnd === FUNCTION) && onAllTimerEnd();
!clearing && !sealed && (typeof onAllTimerEnd === FUNCTION) && onAllTimerEnd();
computeTimerEvents.started = false;
}
}
Expand Down
35 changes: 35 additions & 0 deletions test/unit/sandbox-error-events.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,39 @@ describe('sandbox error events', function () {
});
});
});

it('should forward errors from top level await', function (done) {
Sandbox.createContext(function (err, ctx) {
if (err) { return done(err); }

const executionError = sinon.spy(),
executionErrorSpecific = sinon.spy();

ctx.on('execution.error', executionError);
ctx.on('execution.error.exec-id', executionErrorSpecific);

ctx.execute(`
async function makeMeThrow () {
await Promise.reject(new Error('catch me if you can'));
}
await makeMeThrow();
`, { id: 'exec-id' }, function (err) {
expect(err).to.be.an('object').and.have.property('message', 'catch me if you can');

expect(executionError).to.have.been.calledOnce;
expect(executionErrorSpecific).to.have.been.calledOnce;

expect(executionError.args[0][0]).to.be.an('object').and.have.property('execution', 'exec-id');
expect(executionError.args[0][1]).to.be.an('object')
.and.have.property('message', 'catch me if you can');

expect(executionErrorSpecific.args[0][0]).to.be.an('object').and.have.property('execution', 'exec-id');
expect(executionErrorSpecific.args[0][1]).to.be.an('object')
.and.have.property('message', 'catch me if you can');

done();
});
});
});
});
16 changes: 16 additions & 0 deletions test/unit/sandbox-sanity.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ describe('sandbox', function () {
});
});

it('should execute code with top level await', function (done) {
Sandbox.createContext(function (err, ctx) {
if (err) { return done(err); }

ctx.on('error', done);

ctx.execute(`
async function main () {
await Promise.resolve();
}
await main();
`, done);
});
});

it('should have a few important globals', function (done) {
Sandbox.createContext(function (err, ctx) {
if (err) { return done(err); }
Expand Down

0 comments on commit d8318c1

Please sign in to comment.