diff --git a/src/NetscriptFunctions.js b/src/NetscriptFunctions.js index 8ad911b79..dd23c18c7 100644 --- a/src/NetscriptFunctions.js +++ b/src/NetscriptFunctions.js @@ -1143,7 +1143,7 @@ function NetscriptFunctions(workerScript) { var oldScript = destServer.scripts[i]; oldScript.code = sourceScript.code; oldScript.ramUsage = sourceScript.ramUsage; - oldScript.module = ""; + oldScript.markUpdated(); return true; } } @@ -1947,6 +1947,7 @@ function NetscriptFunctions(workerScript) { } mode === "w" ? script.code = data : script.code += data; script.updateRamUsage(server.scripts); + script.markUpdated(); } else { // Write to text file let txtFile = getTextFile(fn, server); diff --git a/src/NetscriptJSEvaluator.js b/src/NetscriptJSEvaluator.js index c1632b0ae..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,8 +37,9 @@ 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 = new Promise(resolve => resolve(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 = await script.module; @@ -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/NetscriptWorker.js b/src/NetscriptWorker.js index 319693358..095ba401d 100644 --- a/src/NetscriptWorker.js +++ b/src/NetscriptWorker.js @@ -534,7 +534,7 @@ export function loadAllRunningScripts() { // Reset modules on all scripts for (let i = 0; i < server.scripts.length; ++i) { - server.scripts[i].module = ""; + server.scripts[i].markUpdated(); } if (skipScriptLoad) { diff --git a/src/Script/Script.ts b/src/Script/Script.ts index cb3200a6c..6e11fd08b 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); } }; @@ -68,6 +79,14 @@ export class Script { } } + /** + * Marks this script as having been updated. It will be recompiled next time something tries + * to exec it. + */ + markUpdated() { + this.module = ""; + } + /** * Save a script from the script editor * @param {string} code - The new contents of the script @@ -86,10 +105,16 @@ export class Script { this.filename = filenameElem!.value; this.server = serverIp; this.updateRamUsage(otherScripts); - this.module = ""; + 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/src/Server/BaseServer.ts b/src/Server/BaseServer.ts index 74f297189..143570abf 100644 --- a/src/Server/BaseServer.ts +++ b/src/Server/BaseServer.ts @@ -233,7 +233,7 @@ export class BaseServer { let script = this.scripts[i]; script.code = code; script.updateRamUsage(this.scripts); - script.module = ""; + script.markUpdated(); ret.overwritten = true; ret.success = true; return ret;