diff --git a/binaries/data/mods/_test.scriptinterface/module/empty~trigger.append.js b/binaries/data/mods/_test.scriptinterface/module/empty~trigger.append.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/scriptinterface/ModuleLoader.cpp b/source/scriptinterface/ModuleLoader.cpp index eaa0d2ff05..fa21894312 100644 --- a/source/scriptinterface/ModuleLoader.cpp +++ b/source/scriptinterface/ModuleLoader.cpp @@ -114,16 +114,19 @@ VfsPath GetBaseFilename(const VfsPath& filename) return file.DecodeUTF8(); } +template [[nodiscard]] JSObject* CompileModule(const ScriptRequest& rq, ModuleLoader::RegistryType& registry, - const VfsPath& filePath) + const VfsPath& filePath, Requester&& requester) { const VfsPath normalizedPath{filePath.fileSystemPath().lexically_normal().generic_string()}; const auto insertResult = registry.try_emplace(normalizedPath, rq, normalizedPath); - return std::get<1>(*std::get<0>(insertResult)).m_ModuleObject; + ModuleLoader::CompiledModule& compiledModule{std::get<1>(*std::get<0>(insertResult))}; + compiledModule.AddRequester(std::forward(requester)); + return compiledModule.m_ModuleObject; } -[[nodiscard]] JSObject* Resolve(const ScriptRequest& rq, - ModuleLoader::RegistryType& registry, JS::HandleObject moduleRequest) +[[nodiscard]] JSObject* Resolve(const ScriptRequest& rq, ModuleLoader::RegistryType& registry, + JS::HandleValue referencingModule, JS::HandleObject moduleRequest) { std::string includeString; const JS::RootedValue pathValue{rq.cx, @@ -131,7 +134,11 @@ VfsPath GetBaseFilename(const VfsPath& filename) if (!Script::FromJSVal(rq, pathValue, includeString)) throw std::logic_error{"The module-name to import isn't a string."}; - return CompileModule(rq, registry, includeString); + std::string includingModule; + if (!Script::FromJSProperty(rq, referencingModule, "path", includingModule)) + throw std::logic_error{"The importing module doesn't have a \"path\" property."}; + + return CompileModule(rq, registry, includeString, includingModule); } [[nodiscard]] JSObject* Evaluate(const ScriptRequest& rq, JS::HandleObject mod) @@ -152,6 +159,39 @@ VfsPath GetBaseFilename(const VfsPath& filename) return &val.toObject(); } +Status FileChangedHook(void* param, const VfsPath& changedFile) +{ + ModuleLoader::RegistryType& registry{*static_cast(param)}; + + const VfsPath proposedBasePath{GetBaseFilename(changedFile)}; + + std::vector modulesToErase{proposedBasePath.empty() ? changedFile : proposedBasePath}; + std::vector> queries; + while (!modulesToErase.empty()) + { + const VfsPath path{modulesToErase.back()}; + modulesToErase.pop_back(); + const VfsPath pathWithExtension{path.ChangeExtension(".js")}; + const auto it = registry.find(pathWithExtension); + if (it == registry.end()) + continue; + + ModuleLoader::CompiledModule compiledModule{std::move(std::get<1>(*it))}; + registry.erase(it); + + const auto [additionalModules, callbacks] = compiledModule.GetRequesters(); + modulesToErase.insert(modulesToErase.end(), + additionalModules.begin(), additionalModules.end()); + + queries.insert(queries.end(), callbacks.begin(), callbacks.end()); + } + + for (ModuleLoader::Result& result : queries) + result.Resume(); + + return INFO::OK; +} + template bool Call(JSContext* cx, const unsigned argc, JS::Value* vp) { @@ -228,7 +268,34 @@ ModuleLoader::CompiledModule::CompiledModule(const ScriptRequest& rq, const VfsP JS::SetModulePrivate(m_ModuleObject, modInfo); } -ModuleLoader::Future::Future(const ScriptRequest& rq, ModuleLoader& loader, const VfsPath& modulePath): +[[nodiscard]] std::tuple&, + const std::vector>&> + ModuleLoader::CompiledModule::GetRequesters() const +{ + return {m_Importer, m_Callbacks}; +} + +void ModuleLoader::CompiledModule::AddRequester(VfsPath importer) +{ + m_Importer.push_back(std::move(importer)); +} + +void ModuleLoader::CompiledModule::AddRequester(Result& callback) +{ + m_Callbacks.push_back(callback); +} + +void ModuleLoader::CompiledModule::RemoveRequester(Result* toErase) +{ + m_Callbacks.erase(std::remove_if(m_Callbacks.begin(), m_Callbacks.end(), + [&](Result& elem) + { + return &elem == toErase; + }), m_Callbacks.end()); +} + +ModuleLoader::Future::Future(const ScriptRequest& rq, RegistryType& registry, Result& result, + VfsPath modulePath): m_Status{Evaluating{{rq.cx, nullptr}, {rq.cx, JS_NewObject(rq.cx, &callbackClass)}, {rq.cx, JS_NewObject(rq.cx, &callbackClass)}}} { @@ -240,7 +307,7 @@ ModuleLoader::Future::Future(const ScriptRequest& rq, ModuleLoader& loader, cons // - Accessing values which are not yet exported results in an error. These errors might implicitly be // dropped. - JS::RootedObject mod{rq.cx, CompileModule(rq, loader.m_Registry, modulePath)}; + JS::RootedObject mod{rq.cx, CompileModule(rq, registry, modulePath, result)}; JS::RootedObject promise{rq.cx, Evaluate(rq, mod)}; Evaluating& evaluatingStatus{std::get(m_Status)}; evaluatingStatus.moduleNamespace = JS::GetModuleNamespace(rq.cx, mod); @@ -285,6 +352,16 @@ ModuleLoader::Future::~Future() std::rethrow_exception(std::move(error)); } +[[nodiscard]] bool ModuleLoader::Future::IsWaiting() const noexcept +{ + return std::holds_alternative(m_Status); +} + +void ModuleLoader::Future::SetWaiting() noexcept +{ + m_Status.emplace(); +} + void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept { Evaluating* evaluatingStatus{std::get_if(&m_Status)}; @@ -296,10 +373,90 @@ void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept JS::SetReservedSlot(evaluatingStatus->reject, 0, privateValue); } -[[nodiscard]] ModuleLoader::Future ModuleLoader::LoadModule(const ScriptRequest& rq, +ModuleLoader::Result::iterator::iterator(Result& backReference): + backRef{&backReference} +{} + +[[nodiscard]] ModuleLoader::Future& ModuleLoader::Result::iterator::operator*() const +{ + return backRef->m_Storage; +} + +[[nodiscard]] ModuleLoader::Future* ModuleLoader::Result::iterator::operator->() const +{ + return &(**this); +} + +ModuleLoader::Result::iterator& ModuleLoader::Result::iterator::operator++() +{ + backRef->m_Storage.SetWaiting(); + return *this; +} + +ModuleLoader::Result::iterator& ModuleLoader::Result::iterator::operator++(int) +{ + ++(*this); + // All iterator of this `LoadModuleResult` refere to the same `LoadModuleResult`. + return *this; +} + +[[nodiscard]] bool ModuleLoader::Result::iterator::operator==(const iterator&) +{ + return false; +} + +[[nodiscard]] bool ModuleLoader::Result::iterator::operator!=(const iterator&) +{ + return true; +} + +ModuleLoader::Result::Result(const ScriptRequest& rq, const VfsPath& modulePath): + m_Cx{rq.cx}, + m_Registry{rq.GetScriptInterface().GetModuleLoader().m_Registry}, + m_ModulePath{modulePath}, + m_Storage{rq, m_Registry, *this, m_ModulePath} +{ +} + +ModuleLoader::Result::~Result() +{ + const auto modIter = m_Registry.find(m_ModulePath); + if (modIter == m_Registry.end()) + return; + + std::get<1>(*modIter).RemoveRequester(this); +} + +[[nodiscard]] ModuleLoader::Result::iterator ModuleLoader::Result::begin() noexcept +{ + return ModuleLoader::Result::iterator{*this}; +} + +[[nodiscard]] ModuleLoader::Result::iterator ModuleLoader::Result::end() const noexcept +{ + return ModuleLoader::Result::iterator{}; +} + +void ModuleLoader::Result::Resume() +{ + if (m_Storage.IsWaiting()) + m_Storage = ModuleLoader::Future{m_Cx, m_Registry, *this, m_ModulePath}; +} + +ModuleLoader::ModuleLoader() +{ + RegisterFileReloadFunc(FileChangedHook, static_cast(&m_Registry)); +} + +ModuleLoader::~ModuleLoader() +{ + UnregisterFileReloadFunc(FileChangedHook, static_cast(&m_Registry)); +} + +[[nodiscard]] ModuleLoader::Result ModuleLoader::LoadModule(const ScriptRequest& rq, const VfsPath& modulePath) { - return Future{rq, *this, modulePath}; + return Result{rq, modulePath}; } /** @@ -322,13 +479,14 @@ void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept return true; } -[[nodiscard]] JSObject* ModuleLoader::ResolveHook(JSContext* cx, JS::HandleValue, - JS::HandleObject moduleRequest) noexcept +[[nodiscard]] JSObject* ModuleLoader::ResolveHook(JSContext* cx, JS::HandleValue referencingPrivate, + JS::HandleObject request) noexcept { try { const ScriptRequest rq{cx}; - return Resolve(rq, rq.GetScriptInterface().GetModuleLoader().m_Registry, moduleRequest); + return Resolve(rq, rq.GetScriptInterface().GetModuleLoader().m_Registry, referencingPrivate, + request); } catch (const std::exception& e) { @@ -349,7 +507,7 @@ void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept try { JS::RootedObject mod{rq.cx, Resolve(rq, rq.GetScriptInterface().GetModuleLoader().m_Registry, - moduleRequest)}; + referencingPrivate, moduleRequest)}; JS::RootedObject evaluationPromise{rq.cx, Evaluate(rq, mod)}; return JS::FinishDynamicModuleImport(rq.cx, evaluationPromise, referencingPrivate, moduleRequest, promise); diff --git a/source/scriptinterface/ModuleLoader.h b/source/scriptinterface/ModuleLoader.h index 8a56ac7cf2..2c04e1f660 100644 --- a/source/scriptinterface/ModuleLoader.h +++ b/source/scriptinterface/ModuleLoader.h @@ -21,9 +21,12 @@ #include "lib/file/vfs/vfs_path.h" #include "scriptinterface/ScriptTypes.h" +#include #include +#include #include #include +#include class ScriptContext; class ScriptRequest; @@ -35,80 +38,162 @@ class ModuleLoader public: friend ScriptContext; - class CompiledModule - { - public: - CompiledModule(const ScriptRequest& rq, const VfsPath& filePath); - JS::PersistentRootedObject m_ModuleObject; - }; + class CompiledModule; + class Future; + class Result; using RegistryType = std::unordered_map; - class Future - { - public: - struct Evaluating - { - JS::PersistentRootedObject moduleNamespace; - JS::PersistentRootedObject fulfill; - JS::PersistentRootedObject reject; - }; - struct Fulfilled - { - JS::PersistentRootedObject moduleNamespace; - }; - struct Rejected - { - std::exception_ptr error; - }; - struct Invalid {}; - using Status = std::variant; - - explicit Future(const ScriptRequest& rq, ModuleLoader& loader, const VfsPath& modulePath); - Future() = default; - Future(const Future&) = delete; - Future& operator=(const Future&) = delete; - Future(Future&& other) noexcept; - Future& operator=(Future&& other) noexcept; - ~Future(); - - [[nodiscard]] bool IsDone() const noexcept; - - /** - * Throws if the evaluation of the module failed. - * @return The module namespace. All exported values are a property - * of this object. @c default is a property with name "default". - */ - [[nodiscard]] JSObject* Get(); - - private: - // It's save to not require a `JS::HandleValue` here. - void SetReservedSlot(JS::Value privateValue) noexcept; - - Status m_Status{Invalid{}}; - }; + ModuleLoader(); + ModuleLoader(const ModuleLoader&) = delete; + ModuleLoader& operator=(const ModuleLoader&) = delete; + ModuleLoader(ModuleLoader&&) = delete; + ModuleLoader& operator=(ModuleLoader&&) = delete; + ~ModuleLoader(); /** * Load the specified module and all module it imports recursively. * * @param rq @c globalThis is taken from this @c ScriptRequest. - * @param modulePath The path to the file which should be loaded as a module. - * @return A future that is fulfilled when the evaluation of the module - * completes. + * @param modulePath The path to the file which should be loaded as a + * module. + * @return A range of futures. The compilation of the first future is + * already started. The evaluation of the subsequent futures start once + * the module file is edited. */ - [[nodiscard]] Future LoadModule(const ScriptRequest& rq, const VfsPath& modulePath); + [[nodiscard]] Result LoadModule(const ScriptRequest& rq, const VfsPath& modulePath); private: // Functions used by the `ScriptContext`. [[nodiscard]] static bool MetadataHook(JSContext* cx, JS::HandleValue privateValue, JS::HandleObject metaObject) noexcept; - [[nodiscard]] static JSObject* ResolveHook(JSContext* cx, JS::HandleValue, + [[nodiscard]] static JSObject* ResolveHook(JSContext* cx, JS::HandleValue referencingPrivate, JS::HandleObject moduleRequest) noexcept; [[nodiscard]] static bool DynamicImportHook(JSContext* cx, JS::HandleValue referencingPrivate, JS::HandleObject moduleRequest, JS::HandleObject promise) noexcept; RegistryType m_Registry; }; + +class ModuleLoader::CompiledModule +{ +public: + CompiledModule(const ScriptRequest& rq, const VfsPath& filePath); + + std::tuple&, + const std::vector>&> GetRequesters() const; + + void AddRequester(VfsPath importer); + void AddRequester(Result& callback); + void RemoveRequester(Result* toErase); + + JS::PersistentRootedObject m_ModuleObject; +private: + std::vector m_Importer; + std::vector> m_Callbacks; +}; + +/** + * The future is fulfilled once the evaluation of the module + * completes. + * Note: The evaluation might not be started yet. + */ +class ModuleLoader::Future +{ + friend Result; +public: + struct Evaluating + { + JS::PersistentRootedObject moduleNamespace; + JS::PersistentRootedObject fulfill; + JS::PersistentRootedObject reject; + }; + struct Fulfilled + { + JS::PersistentRootedObject moduleNamespace; + }; + struct Rejected + { + std::exception_ptr error; + }; + struct WaitingForFileChange {}; + struct Invalid {}; + using Status = std::variant; + + explicit Future(const ScriptRequest& rq, RegistryType& reqistry, Result& result, VfsPath modulePath); + Future() = default; + Future(const Future&) = delete; + Future& operator=(const Future&) = delete; + Future(Future&& other) noexcept; + Future& operator=(Future&& other) noexcept; + ~Future(); + + [[nodiscard]] bool IsDone() const noexcept; + + /** + * Throws if the evaluation of the module failed. + * @return The module namespace. All exported values are a property + * of this object. @c default is a property with name "default". + */ + [[nodiscard]] JSObject* Get(); + +private: + [[nodiscard]] bool IsWaiting() const noexcept; + void SetWaiting() noexcept; + + // It's save to not require a `JS::HandleValue` here. + void SetReservedSlot(JS::Value privateValue) noexcept; + + Status m_Status{Invalid{}}; +}; + +class ModuleLoader::Result +{ + class iterator; +public: + explicit Result(const ScriptRequest& rq, const VfsPath& modulePath); + Result(const Result&) = delete; + Result& operator=(const Result&) = delete; + Result(Result&&) = delete; + Result& operator=(Result&&) = delete; + ~Result(); + + [[nodiscard]] iterator begin() noexcept; + [[nodiscard]] iterator end() const noexcept; + + void Resume(); + +private: + JSContext* m_Cx; + RegistryType& m_Registry; + VfsPath m_ModulePath; + Future m_Storage; +}; + +class ModuleLoader::Result::iterator +{ +public: + using difference_type = std::ptrdiff_t; + using value_type = Future; + using pointer = value_type*; + using reference = value_type&; + using iterator_category = std::input_iterator_tag; + + explicit iterator() = default; + explicit iterator(Result& backReference); + + [[nodiscard]] reference operator*() const; + [[nodiscard]] pointer operator->() const; + + iterator& operator++(); + iterator& operator++(int); + + [[nodiscard]] bool operator==(const iterator&); + [[nodiscard]] bool operator!=(const iterator&); + +private: + Result* backRef{nullptr}; +}; } // namespace Script #endif // INCLUDED_SCRIPTMODULELOADER diff --git a/source/scriptinterface/tests/test_Module.h b/source/scriptinterface/tests/test_Module.h index 8898d697f7..2f91a11668 100644 --- a/source/scriptinterface/tests/test_Module.h +++ b/source/scriptinterface/tests/test_Module.h @@ -17,6 +17,8 @@ #include "lib/self_test.h" +#include "lib/file/vfs/vfs.h" +#include "lib/sysdep/dir_watch.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "scriptinterface/FunctionWrapper.h" @@ -26,11 +28,53 @@ #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" +#if OS_LINUX +#include +#endif +#if OS_WIN || OS_WIN64 || OS_MAC || OS_MACOSX +#include +#endif +#include + +Status wdir_watch_Init(); +Status wdir_watch_Shutdown(); + +namespace +{ +void ClearFromCache(const VfsPath& path) +{ +#if OS_BSD + TS_SKIP("On BSD hotload isn't implemented."); +#endif + + OsPath file; + if (g_VFS->GetRealPath(path, file) != INFO::OK) + throw std::exception{}; + PDirWatch dirWatch; + dir_watch_Add((file.Parent() / "").string8(), dirWatch); + +#if OS_WIN || OS_WIN64 || OS_MAC || OS_MACOSX + std::filesystem::last_write_time(file.string8(), std::filesystem::file_time_type::clock::now()); +#endif + +#if OS_LINUX + // On Linux only this aproach seems to trigger a file reload. + if (std::system(("touch " + file.string8()).c_str()) != 0) + throw std::runtime_error{"`touch` didn't work."}; +#endif + + ReloadChangedFiles(); +} +} + class TestScriptModule : public CxxTest::TestSuite { public: void setUp() { + if constexpr (OS_WIN) + wdir_watch_Init(); + g_VFS = CreateVfs(); TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "_test.scriptinterface" / "module" / "", VFS_MOUNT_MUST_EXIST)); @@ -39,6 +83,9 @@ public: void tearDown() { g_VFS.reset(); + + if constexpr (OS_WIN) + wdir_watch_Shutdown(); } void test_StaticImport() @@ -123,8 +170,9 @@ public: { ScriptInterface script{"Test", "Test", g_ScriptContext}; const ScriptRequest rq{script}; - auto future = script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js"); + auto result = script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js"); + auto& future = *result.begin(); TS_ASSERT(!future.IsDone()); g_ScriptContext->RunJobs(); TS_ASSERT(future.IsDone()); @@ -136,8 +184,9 @@ public: ScriptInterface script{"Test", "Test", g_ScriptContext}; const ScriptRequest rq{script}; - auto future = script.GetModuleLoader().LoadModule(rq, "top_level_await_infinite.js"); + auto result = script.GetModuleLoader().LoadModule(rq, "top_level_await_infinite.js"); + auto& future = *result.begin(); g_ScriptContext->RunJobs(); TS_ASSERT(!future.IsDone()); TS_ASSERT_THROWS_ANYTHING(std::ignore = future.Get()); @@ -148,8 +197,8 @@ public: ScriptInterface script{"Test", "Test", g_ScriptContext}; const ScriptRequest rq{script}; - Script::ModuleLoader::Future future0{ - script.GetModuleLoader().LoadModule(rq, "empty.js")}; + auto result{script.GetModuleLoader().LoadModule(rq, "empty.js")}; + Script::ModuleLoader::Future& future0{*result.begin()}; g_ScriptContext->RunJobs(); TS_ASSERT(future0.IsDone()); @@ -168,8 +217,9 @@ public: ScriptInterface script{"Test", "Test", g_ScriptContext}; const ScriptRequest rq{script}; - Script::ModuleLoader::Future future0{ - script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js")}; + auto result{script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js")}; + Script::ModuleLoader::Future& future0{*result.begin()}; + Script::ModuleLoader::Future future1{std::move(future0)}; Script::ModuleLoader::Future future2; future2 = std::move(future1); @@ -189,11 +239,13 @@ public: const ScriptRequest rq{script}; TestLogger logger; - auto future{script.GetModuleLoader().LoadModule(rq, "delayed_blabbermouth.js")}; + auto blabbermouthResult{script.GetModuleLoader().LoadModule(rq, "delayed_blabbermouth.js")}; TS_ASSERT_STR_NOT_CONTAINS(logger.GetOutput(), "blah blah blah"); + auto future = std::move(*blabbermouthResult.begin()); TS_ASSERT(!future.IsDone()); - future = script.GetModuleLoader().LoadModule(rq, "empty.js"); + auto emptyResult{script.GetModuleLoader().LoadModule(rq, "empty.js")}; + future = std::move(*emptyResult.begin()); TS_ASSERT(!future.IsDone()); g_ScriptContext->RunJobs(); @@ -208,8 +260,9 @@ public: // To silence the error. const TestLogger _; - auto future = script.GetModuleLoader().LoadModule(rq, "top_level_throw.js"); + auto result = script.GetModuleLoader().LoadModule(rq, "top_level_throw.js"); + auto& future = *result.begin(); g_ScriptContext->RunJobs(); TS_ASSERT(future.IsDone()); TS_ASSERT_THROWS_EQUALS(std::ignore = future.Get(), const std::runtime_error& e, e.what(), @@ -221,9 +274,10 @@ public: ScriptInterface script{"Test", "Test", g_ScriptContext}; const ScriptRequest rq{script}; - auto future = script.GetModuleLoader().LoadModule(rq, "export.js"); - + auto result = script.GetModuleLoader().LoadModule(rq, "export.js"); g_ScriptContext->RunJobs(); + + auto& future = *result.begin(); JS::RootedObject ns{rq.cx, future.Get()}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; @@ -248,17 +302,17 @@ public: const ScriptRequest rq{script}; { - auto future = script.GetModuleLoader().LoadModule(rq, "export.js"); + auto result = script.GetModuleLoader().LoadModule(rq, "export.js"); g_ScriptContext->RunJobs(); - JS::RootedObject ns{rq.cx, future.Get()}; + JS::RootedObject ns{rq.cx, result.begin()->Get()}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; TS_ASSERT(ScriptFunction::CallVoid(rq, moduleValue, "mutate", 12)); } { - auto future = script.GetModuleLoader().LoadModule(rq, "include/../export.js"); + auto result = script.GetModuleLoader().LoadModule(rq, "include/../export.js"); g_ScriptContext->RunJobs(); - JS::RootedObject ns{rq.cx, future.Get()}; + JS::RootedObject ns{rq.cx, result.begin()->Get()}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; int value{0}; TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value)); @@ -272,17 +326,17 @@ public: const ScriptRequest rq{script}; { - auto future = script.GetModuleLoader().LoadModule(rq, "export.js"); + auto result = script.GetModuleLoader().LoadModule(rq, "export.js"); g_ScriptContext->RunJobs(); - JS::RootedObject ns{rq.cx, future.Get()}; + JS::RootedObject ns{rq.cx, result.begin()->Get()}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; TS_ASSERT(ScriptFunction::CallVoid(rq, moduleValue, "mutate", 12)); } { - auto future = script.GetModuleLoader().LoadModule(rq, "indirect.js"); + auto result = script.GetModuleLoader().LoadModule(rq, "indirect.js"); g_ScriptContext->RunJobs(); - JS::RootedObject ns{rq.cx, future.Get()}; + JS::RootedObject ns{rq.cx, result.begin()->Get()}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; int value{0}; TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value)); @@ -295,10 +349,10 @@ public: ScriptInterface script{"Test", "Test", g_ScriptContext}; const ScriptRequest rq{script}; - auto future = script.GetModuleLoader().LoadModule(rq, "export_default/immutable.js"); - + auto result = script.GetModuleLoader().LoadModule(rq, "export_default/immutable.js"); g_ScriptContext->RunJobs(); + auto& future = *result.begin(); JS::RootedObject ns{rq.cx, future.Get()}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; @@ -324,10 +378,11 @@ public: ScriptInterface script{"Test", "Test", g_ScriptContext}; const ScriptRequest rq{script}; - auto future = script.GetModuleLoader().LoadModule(rq, "export_default/does_not_work_around.js"); + auto result = script.GetModuleLoader().LoadModule(rq, "export_default/does_not_work_around.js"); g_ScriptContext->RunJobs(); + auto& future = *result.begin(); JS::RootedObject ns{rq.cx, future.Get()}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; @@ -342,10 +397,10 @@ public: ScriptInterface script{"Test", "Test", g_ScriptContext}; const ScriptRequest rq{script}; - auto future = script.GetModuleLoader().LoadModule(rq, "export_default/works_around.js"); - + auto result = script.GetModuleLoader().LoadModule(rq, "export_default/works_around.js"); g_ScriptContext->RunJobs(); + auto& future = *result.begin(); JS::RootedObject ns{rq.cx, future.Get()}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; @@ -359,8 +414,10 @@ public: ScriptInterface script{"Test", "Test", g_ScriptContext}; const ScriptRequest rq{script}; - auto future = script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js"); - future = script.GetModuleLoader().LoadModule(rq, "export.js"); + auto awaitResult = script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js"); + auto future = std::move(*awaitResult.begin()); + auto exportResult = script.GetModuleLoader().LoadModule(rq, "export.js"); + future = std::move(*exportResult.begin()); g_ScriptContext->RunJobs(); JS::RootedObject ns{rq.cx, future.Get()}; @@ -376,10 +433,10 @@ public: ScriptInterface script{"Test", "Test", g_ScriptContext}; const ScriptRequest rq{script}; - auto future = script.GetModuleLoader().LoadModule(rq, "dynamic_import.js"); - + auto result = script.GetModuleLoader().LoadModule(rq, "dynamic_import.js"); g_ScriptContext->RunJobs(); + auto& future = *result.begin(); JS::RootedObject ns{rq.cx, future.Get()}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; @@ -407,10 +464,10 @@ public: ScriptInterface script{"Test", "Test", g_ScriptContext}; const ScriptRequest rq{script}; - auto future = script.GetModuleLoader().LoadModule(rq, "meta.js"); - + auto result = script.GetModuleLoader().LoadModule(rq, "meta.js"); g_ScriptContext->RunJobs(); + auto& future = *result.begin(); JS::RootedObject ns{rq.cx, future.Get()}; const JS::RootedValue modNamespace{rq.cx, JS::ObjectValue(*ns)}; @@ -427,15 +484,186 @@ public: ScriptInterface script{"Test", "Test", g_ScriptContext}; const ScriptRequest rq{script}; - auto future = script.GetModuleLoader().LoadModule(rq, "modified/base.js"); - + auto result = script.GetModuleLoader().LoadModule(rq, "modified/base.js"); g_ScriptContext->RunJobs(); + auto& future = *result.begin(); JS::RootedObject ns{rq.cx, future.Get()}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; - std::string result; - TS_ASSERT(ScriptFunction::Call(rq, moduleValue, "fn", result)); - TS_ASSERT_STR_EQUALS(result, "Base01"); + std::string returnValue; + TS_ASSERT(ScriptFunction::Call(rq, moduleValue, "fn", returnValue)); + TS_ASSERT_STR_EQUALS(returnValue, "Base01"); + } + + void test_Hotload() + { + constexpr int goal{2}; + + ScriptInterface script{"Test", "Test", g_ScriptContext}; + const ScriptRequest rq{script}; + + int counter{0}; + for (auto& future : script.GetModuleLoader().LoadModule(rq, "empty.js")) + { + TS_ASSERT(!future.IsDone()); + + if (counter != 0) + ClearFromCache("empty.js"); + + g_ScriptContext->RunJobs(); + TS_ASSERT(future.IsDone()); + + if (counter == goal) + break; + + ++counter; + } + + TS_ASSERT_EQUALS(counter, goal); + } + + void test_HotloadWithoutIncrement() + { + ScriptInterface script{"Test", "Test", g_ScriptContext}; + const ScriptRequest rq{script}; + + auto result = script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js"); + g_ScriptContext->RunJobs(); + TS_ASSERT(result.begin()->IsDone()); + ClearFromCache("top_level_await_finite.js"); + TS_ASSERT(result.begin()->IsDone()); + } + + void test_HotloadIndipendence() + { + ScriptInterface script{"Test", "Test", g_ScriptContext}; + const ScriptRequest rq{script}; + + // It's intended to be used as in the test above but it's easier to test when it's unrolled. + auto result = script.GetModuleLoader().LoadModule(rq, "export.js"); + auto iter = result.begin(); + { + auto& future = *iter; + g_ScriptContext->RunJobs(); + JS::RootedObject ns{rq.cx, future.Get()}; + const JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; + + TS_ASSERT(ScriptFunction::CallVoid(rq, moduleValue, "mutate", 12)); + + int value{0}; + TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value)); + TS_ASSERT_EQUALS(value, 12); + } + ++iter; + { + auto& future = *iter; + g_ScriptContext->RunJobs(); + TS_ASSERT(!future.IsDone()); + ClearFromCache("export.js"); + g_ScriptContext->RunJobs(); + TS_ASSERT(future.IsDone()); + + JS::RootedObject ns{rq.cx, future.Get()}; + JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; + + int value{0}; + TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value)); + TS_ASSERT_DIFFERS(value, 12); + TS_ASSERT_EQUALS(value, 6); + } + } + + void test_HotloadModified() + { + ScriptInterface script{"Test", "Test", g_ScriptContext}; + const ScriptRequest rq{script}; + + auto result = script.GetModuleLoader().LoadModule(rq, "empty.js"); + auto iter = result.begin(); + g_ScriptContext->RunJobs(); + TS_ASSERT(iter->IsDone()); + + ++iter; + TS_ASSERT(!iter->IsDone()); + + ClearFromCache("empty~trigger.append.js"); + + g_ScriptContext->RunJobs(); + TS_ASSERT(iter->IsDone()); + } + + void test_HotloadIndirect() + { + ScriptInterface script{"Test", "Test", g_ScriptContext}; + const ScriptRequest rq{script}; + + auto result = script.GetModuleLoader().LoadModule(rq, "indirect.js"); + auto iter = result.begin(); + g_ScriptContext->RunJobs(); + TS_ASSERT(iter->IsDone()); + + ++iter; + ClearFromCache("export.js"); + + g_ScriptContext->RunJobs(); + TS_ASSERT(iter->IsDone()); + } + + void test_HotloadUnobserved() + { + ScriptInterface script{"Test", "Test", g_ScriptContext}; + { + const ScriptRequest rq{script}; + + TestLogger logger; + auto result = script.GetModuleLoader().LoadModule(rq, "blabbermouth.js"); + g_ScriptContext->RunJobs(); + TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "blah blah blah"); + } + + { + TestLogger logger; + ClearFromCache("blabbermouth.js"); + g_ScriptContext->RunJobs(); + TS_ASSERT_STR_NOT_CONTAINS(logger.GetOutput(), "blah blah blah"); + } + + { + const ScriptRequest rq{script}; + + TestLogger logger; + auto result = script.GetModuleLoader().LoadModule(rq, "blabbermouth.js"); + g_ScriptContext->RunJobs(); + TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "blah blah blah"); + } + } + + void test_HotloadAfterResultDestruction() + { + ScriptInterface script{"Test", "Test", g_ScriptContext}; + { + const ScriptRequest rq{script}; + + TestLogger logger; + auto result = script.GetModuleLoader().LoadModule(rq, "blabbermouth.js"); + g_ScriptContext->RunJobs(); + TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "blah blah blah"); + + auto iter = result.begin(); + TS_ASSERT(iter->IsDone()); + ++iter; + } + + TestLogger logger; + ClearFromCache("blabbermouth.js"); + g_ScriptContext->RunJobs(); + TS_ASSERT_STR_NOT_CONTAINS(logger.GetOutput(), "blah blah blah"); + } + + void test_ResultDestructionAfterScriptRequestDestruction() + { + ScriptInterface script{"Test", "Test", g_ScriptContext}; + auto _ = script.GetModuleLoader().LoadModule(ScriptRequest{script}, "empty.js"); } };