diff --git a/src/NetscriptFunctions.js b/src/NetscriptFunctions.js index 92e64bf9c..5b6f6bb8d 100644 --- a/src/NetscriptFunctions.js +++ b/src/NetscriptFunctions.js @@ -1139,7 +1139,7 @@ function NetscriptFunctions(workerScript) { var oldScript = destServer.scripts[i]; oldScript.code = sourceScript.code; oldScript.ramUsage = sourceScript.ramUsage; - oldScript.markUpdated();; + oldScript.markUpdated(); return true; } } diff --git a/src/NetscriptJSEvaluator.js b/src/NetscriptJSEvaluator.js index 0f8032f90..5325911a3 100644 --- a/src/NetscriptJSEvaluator.js +++ b/src/NetscriptJSEvaluator.js @@ -1,10 +1,22 @@ import { makeRuntimeRejectMsg } from "./NetscriptEvaluator"; +import { Script } from "./Script/Script"; // Makes a blob that contains the code of a given script. export function makeScriptBlob(code) { return new Blob([code], {type: "text/javascript"}); } +class ScriptUrl { + /** + * @param {string} filename + * @param {string} url + */ + constructor(filename, url) { + this.filename = filename; + this.url = url; + } +} + // Begin executing a user JS script, and return a promise that resolves // or rejects when the script finishes. // - script is a script to execute (see Script.js). We depend only on .filename and .code. @@ -15,9 +27,9 @@ export function makeScriptBlob(code) { // running the main function of the script. export async function executeJSScript(scripts = [], workerScript) { let loadedModule; - let urlStack = null; + let urls = null; let script = workerScript.getScript(); - if (script.module === "") { + if (shouldCompile(script, scripts)) { // The URL at the top is the one we want to import. It will // recursively import all the other modules in the urlStack. // @@ -25,10 +37,11 @@ export async function executeJSScript(scripts = [], workerScript) { // but not really behaves like import. Particularly, it cannot // load fully dynamic content. So we hide the import from webpack // by placing it inside an eval call. - urlStack = _getScriptUrls(script, scripts, []); - script.module = await eval('import(urlStack[urlStack.length - 1])'); + urls = _getScriptUrls(script, scripts, []); + script.module = new Promise(resolve => resolve(eval('import(urls[urls.length - 1].url)'))); + script.dependencies = urls.map(u => u.filename); } - loadedModule = script.module; + loadedModule = await script.module; let ns = workerScript.env.vars; @@ -41,12 +54,31 @@ export async function executeJSScript(scripts = [], workerScript) { return loadedModule.main(ns); } finally { // Revoke the generated URLs - if (urlStack != null) { - for (const url in urlStack) URL.revokeObjectURL(url); + if (urls != null) { + for (const b in urls) URL.revokeObjectURL(b.url); } }; } +/** Returns whether we should compile the script parameter. + * + * @param {Script} script + * @param {Script[]} scripts + */ +function shouldCompile(script, scripts) { + if (script.module === "") return true; + return script.dependencies.some(dep => { + const depScript = scripts.find(s => s.filename == dep); + + // If the script is not present on the server, we should recompile, if only to get any necessary + // compilation errors. + if (!depScript) return true; + + const depIsMoreRecent = depScript.moduleSequenceNumber > script.moduleSequenceNumber + return depIsMoreRecent; + }); +} + // Gets a stack of blob urls, the top/right-most element being // the blob url for the named script on the named server. // @@ -58,8 +90,18 @@ export async function executeJSScript(scripts = [], workerScript) { // different parts of the tree. That hasn't presented any problem with during // testing, but it might be an idea for the future. Would require a topo-sort // then url-izing from leaf-most to root-most. +/** + * @param {Script} script + * @param {Script[]} scripts + * @param {Script[]} seen + * @returns {ScriptUrl[]} All of the compiled scripts, with the final one + * in the list containing the blob corresponding to + * the script parameter. + */ +// BUG: apparently seen is never consulted. Oops. export function _getScriptUrls(script, scripts, seen) { // Inspired by: https://stackoverflow.com/a/43834063/91401 + /** @type {ScriptUrl[]} */ const urlStack = []; seen.push(script); try { @@ -86,7 +128,7 @@ export function _getScriptUrls(script, scripts, seen) { // The top url in the stack is the replacement import file for this script. urlStack.push(...urls); - return [prefix, urls[urls.length - 1], suffix].join(''); + return [prefix, urls[urls.length - 1].url, suffix].join(''); } ); @@ -96,7 +138,7 @@ export function _getScriptUrls(script, scripts, seen) { // If we successfully transformed the code, create a blob url for it and // push that URL onto the top of the stack. - urlStack.push(URL.createObjectURL(makeScriptBlob(transformedCode))); + urlStack.push(new ScriptUrl(script.filename, URL.createObjectURL(makeScriptBlob(transformedCode)))); return urlStack; } catch (err) { // If there is an error, we need to clean up the URLs. diff --git a/src/Script/Script.ts b/src/Script/Script.ts index 9cc395d56..f1a8f3bf0 100644 --- a/src/Script/Script.ts +++ b/src/Script/Script.ts @@ -15,6 +15,8 @@ import { } from "../../utils/JSONReviver"; import { roundToTwo } from "../../utils/helpers/roundToTwo"; +let globalModuleSequenceNumber = 0; + export class Script { // Initializes a Script Object from a JSON save state static fromJSON(value: any): Script { @@ -31,6 +33,14 @@ export class Script { // This is only applicable for NetscriptJS module: any = ""; + // The timestamp when when the script was last updated. + moduleSequenceNumber: number; + + // Only used with NS2 scripts; the list of dependency script filenames. This is constructed + // whenever the script is first evaluated, and therefore may be out of date if the script + // has been updated since it was last run. + dependencies: string[] = []; + // Amount of RAM this Script requres to run ramUsage: number = 0; @@ -43,6 +53,7 @@ export class Script { this.ramUsage = 0; this.server = server; // IP of server this script is on this.module = ""; + this.moduleSequenceNumber = ++globalModuleSequenceNumber; if (this.code !== "") { this.updateRamUsage(otherScripts); } }; @@ -94,10 +105,16 @@ export class Script { this.filename = filenameElem!.value; this.server = serverIp; this.updateRamUsage(otherScripts); - this.markUpdated(); + this.markUpdated(); } } + // Marks that this script has been updated. Causes recompilation of NS2 modules. + markUpdated() { + this.module = ""; + this.moduleSequenceNumber = ++globalModuleSequenceNumber; + } + /** * Calculates and updates the script's RAM usage based on its code * @param {Script[]} otherScripts - Other scripts on the server. Used to process imports diff --git a/utils/DialogBox.js b/utils/DialogBox.js index 127740271..aa165bb94 100644 --- a/utils/DialogBox.js +++ b/utils/DialogBox.js @@ -1,3 +1,5 @@ +import { KEY } from "./helpers/keyCodes"; + /** * Create and display a pop-up dialog box. * This dialog box does not allow for any interaction and should close when clicking @@ -9,28 +11,31 @@ let dialogBoxes = []; $(document).click(function(event) { if (dialogBoxOpened && dialogBoxes.length >= 1) { if (!$(event.target).closest(dialogBoxes[0]).length){ - dialogBoxes[0].remove(); - dialogBoxes.splice(0, 1); - if (dialogBoxes.length == 0) { - dialogBoxOpened = false; - } else { - dialogBoxes[0].style.visibility = "visible"; - } + closeTopmostDialogBox(); } } }); +function closeTopmostDialogBox() { + if (!dialogBoxOpened || dialogBoxes.length === 0) return; + dialogBoxes[0].remove(); + dialogBoxes.shift(); + if (dialogBoxes.length == 0) { + dialogBoxOpened = false; + } else { + dialogBoxes[0].style.visibility = "visible"; + } +} // Dialog box close buttons $(document).on('click', '.dialog-box-close-button', function( event ) { - if (dialogBoxOpened && dialogBoxes.length >= 1) { - dialogBoxes[0].remove(); - dialogBoxes.splice(0, 1); - if (dialogBoxes.length == 0) { - dialogBoxOpened = false; - } else { - dialogBoxes[0].style.visibility = "visible"; - } + closeTopmostDialogBox(); +}); + +document.addEventListener("keydown", function (event) { + if (event.keyCode == KEY.ESC && dialogBoxOpened) { + closeTopmostDialogBox(); + event.preventDefault(); } });