From d8318c10bd72f1bc87fd2cb1c80d8e7167bfcd73 Mon Sep 17 00:00:00 2001 From: Appurva Murawat Date: Mon, 22 Apr 2024 18:01:47 +0530 Subject: [PATCH] Add support for top-level await --- CHANGELOG.yaml | 4 +++ lib/sandbox/execute-context.js | 28 ++++++++------------- lib/sandbox/execute.js | 19 +++++++++----- lib/sandbox/non-legacy-codemarkers.js | 14 +++++++++++ lib/sandbox/timers.js | 2 +- test/unit/sandbox-error-events.test.js | 35 ++++++++++++++++++++++++++ test/unit/sandbox-sanity.test.js | 16 ++++++++++++ 7 files changed, 93 insertions(+), 25 deletions(-) create mode 100644 lib/sandbox/non-legacy-codemarkers.js diff --git a/CHANGELOG.yaml b/CHANGELOG.yaml index 86b30fd9..c1f49cc4 100644 --- a/CHANGELOG.yaml +++ b/CHANGELOG.yaml @@ -1,3 +1,7 @@ +unreleased: + new features: + - Added support for top-level await in scripts + 4.7.1: date: 2024-04-03 fixed bugs: diff --git a/lib/sandbox/execute-context.js b/lib/sandbox/execute-context.js index 07e2bca1..605a5152 100644 --- a/lib/sandbox/execute-context.js +++ b/lib/sandbox/execute-context.js @@ -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 @@ -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 @@ -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); }); }; diff --git a/lib/sandbox/execute.js b/lib/sandbox/execute.js index 1a415e51..7026d4e2 100644 --- a/lib/sandbox/execute.js +++ b/lib/sandbox/execute.js @@ -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.', @@ -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 }), @@ -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; @@ -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'); diff --git a/lib/sandbox/non-legacy-codemarkers.js b/lib/sandbox/non-legacy-codemarkers.js new file mode 100644 index 00000000..7ba9e566 --- /dev/null +++ b/lib/sandbox/non-legacy-codemarkers.js @@ -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]; + } +}; diff --git a/lib/sandbox/timers.js b/lib/sandbox/timers.js index 9e6f4fc7..cfe838ea 100644 --- a/lib/sandbox/timers.js +++ b/lib/sandbox/timers.js @@ -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; } } diff --git a/test/unit/sandbox-error-events.test.js b/test/unit/sandbox-error-events.test.js index f4485753..0677307c 100644 --- a/test/unit/sandbox-error-events.test.js +++ b/test/unit/sandbox-error-events.test.js @@ -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(); + }); + }); + }); }); diff --git a/test/unit/sandbox-sanity.test.js b/test/unit/sandbox-sanity.test.js index d3512b89..8861ffc9 100644 --- a/test/unit/sandbox-sanity.test.js +++ b/test/unit/sandbox-sanity.test.js @@ -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); }