1
0
forked from mirrors/0ad

Allow modules to be hotloaded

Reload a module when it's script file got changed or when one of it's
imported modules got reloaded.

This doesn't work for dynamic `import`s.
This commit is contained in:
phosit
2025-01-29 19:28:48 +01:00
committed by phosit
parent 4043f954a7
commit 6492bc705a
4 changed files with 573 additions and 102 deletions
+171 -13
View File
@@ -114,16 +114,19 @@ VfsPath GetBaseFilename(const VfsPath& filename)
return file.DecodeUTF8(); return file.DecodeUTF8();
} }
template<typename Requester>
[[nodiscard]] JSObject* CompileModule(const ScriptRequest& rq, ModuleLoader::RegistryType& registry, [[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 VfsPath normalizedPath{filePath.fileSystemPath().lexically_normal().generic_string()};
const auto insertResult = registry.try_emplace(normalizedPath, rq, normalizedPath); 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>(requester));
return compiledModule.m_ModuleObject;
} }
[[nodiscard]] JSObject* Resolve(const ScriptRequest& rq, [[nodiscard]] JSObject* Resolve(const ScriptRequest& rq, ModuleLoader::RegistryType& registry,
ModuleLoader::RegistryType& registry, JS::HandleObject moduleRequest) JS::HandleValue referencingModule, JS::HandleObject moduleRequest)
{ {
std::string includeString; std::string includeString;
const JS::RootedValue pathValue{rq.cx, const JS::RootedValue pathValue{rq.cx,
@@ -131,7 +134,11 @@ VfsPath GetBaseFilename(const VfsPath& filename)
if (!Script::FromJSVal(rq, pathValue, includeString)) if (!Script::FromJSVal(rq, pathValue, includeString))
throw std::logic_error{"The module-name to import isn't a string."}; 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) [[nodiscard]] JSObject* Evaluate(const ScriptRequest& rq, JS::HandleObject mod)
@@ -152,6 +159,39 @@ VfsPath GetBaseFilename(const VfsPath& filename)
return &val.toObject(); return &val.toObject();
} }
Status FileChangedHook(void* param, const VfsPath& changedFile)
{
ModuleLoader::RegistryType& registry{*static_cast<ModuleLoader::RegistryType*>(param)};
const VfsPath proposedBasePath{GetBaseFilename(changedFile)};
std::vector<VfsPath> modulesToErase{proposedBasePath.empty() ? changedFile : proposedBasePath};
std::vector<std::reference_wrapper<ModuleLoader::Result>> 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 reject> template<bool reject>
bool Call(JSContext* cx, const unsigned argc, JS::Value* vp) 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); JS::SetModulePrivate(m_ModuleObject, modInfo);
} }
ModuleLoader::Future::Future(const ScriptRequest& rq, ModuleLoader& loader, const VfsPath& modulePath): [[nodiscard]] std::tuple<const std::vector<VfsPath>&,
const std::vector<std::reference_wrapper<ModuleLoader::Result>>&>
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<false>)}, m_Status{Evaluating{{rq.cx, nullptr}, {rq.cx, JS_NewObject(rq.cx, &callbackClass<false>)},
{rq.cx, JS_NewObject(rq.cx, &callbackClass<true>)}}} {rq.cx, JS_NewObject(rq.cx, &callbackClass<true>)}}}
{ {
@@ -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 // - Accessing values which are not yet exported results in an error. These errors might implicitly be
// dropped. // 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)}; JS::RootedObject promise{rq.cx, Evaluate(rq, mod)};
Evaluating& evaluatingStatus{std::get<Evaluating>(m_Status)}; Evaluating& evaluatingStatus{std::get<Evaluating>(m_Status)};
evaluatingStatus.moduleNamespace = JS::GetModuleNamespace(rq.cx, mod); evaluatingStatus.moduleNamespace = JS::GetModuleNamespace(rq.cx, mod);
@@ -285,6 +352,16 @@ ModuleLoader::Future::~Future()
std::rethrow_exception(std::move(error)); std::rethrow_exception(std::move(error));
} }
[[nodiscard]] bool ModuleLoader::Future::IsWaiting() const noexcept
{
return std::holds_alternative<WaitingForFileChange>(m_Status);
}
void ModuleLoader::Future::SetWaiting() noexcept
{
m_Status.emplace<WaitingForFileChange>();
}
void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept
{ {
Evaluating* evaluatingStatus{std::get_if<Evaluating>(&m_Status)}; Evaluating* evaluatingStatus{std::get_if<Evaluating>(&m_Status)};
@@ -296,10 +373,90 @@ void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept
JS::SetReservedSlot(evaluatingStatus->reject, 0, privateValue); 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<void*>(&m_Registry));
}
ModuleLoader::~ModuleLoader()
{
UnregisterFileReloadFunc(FileChangedHook, static_cast<void*>(&m_Registry));
}
[[nodiscard]] ModuleLoader::Result ModuleLoader::LoadModule(const ScriptRequest& rq,
const VfsPath& modulePath) 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; return true;
} }
[[nodiscard]] JSObject* ModuleLoader::ResolveHook(JSContext* cx, JS::HandleValue, [[nodiscard]] JSObject* ModuleLoader::ResolveHook(JSContext* cx, JS::HandleValue referencingPrivate,
JS::HandleObject moduleRequest) noexcept JS::HandleObject request) noexcept
{ {
try try
{ {
const ScriptRequest rq{cx}; 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) catch (const std::exception& e)
{ {
@@ -349,7 +507,7 @@ void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept
try try
{ {
JS::RootedObject mod{rq.cx, Resolve(rq, rq.GetScriptInterface().GetModuleLoader().m_Registry, JS::RootedObject mod{rq.cx, Resolve(rq, rq.GetScriptInterface().GetModuleLoader().m_Registry,
moduleRequest)}; referencingPrivate, moduleRequest)};
JS::RootedObject evaluationPromise{rq.cx, Evaluate(rq, mod)}; JS::RootedObject evaluationPromise{rq.cx, Evaluate(rq, mod)};
return JS::FinishDynamicModuleImport(rq.cx, evaluationPromise, referencingPrivate, return JS::FinishDynamicModuleImport(rq.cx, evaluationPromise, referencingPrivate,
moduleRequest, promise); moduleRequest, promise);
+139 -54
View File
@@ -21,9 +21,12 @@
#include "lib/file/vfs/vfs_path.h" #include "lib/file/vfs/vfs_path.h"
#include "scriptinterface/ScriptTypes.h" #include "scriptinterface/ScriptTypes.h"
#include <cstddef>
#include <exception> #include <exception>
#include <functional>
#include <unordered_map> #include <unordered_map>
#include <variant> #include <variant>
#include <vector>
class ScriptContext; class ScriptContext;
class ScriptRequest; class ScriptRequest;
@@ -35,80 +38,162 @@ class ModuleLoader
public: public:
friend ScriptContext; friend ScriptContext;
class CompiledModule class CompiledModule;
{ class Future;
public: class Result;
CompiledModule(const ScriptRequest& rq, const VfsPath& filePath);
JS::PersistentRootedObject m_ModuleObject;
};
using RegistryType = std::unordered_map<VfsPath, CompiledModule>; using RegistryType = std::unordered_map<VfsPath, CompiledModule>;
class Future ModuleLoader();
{ ModuleLoader(const ModuleLoader&) = delete;
public: ModuleLoader& operator=(const ModuleLoader&) = delete;
struct Evaluating ModuleLoader(ModuleLoader&&) = delete;
{ ModuleLoader& operator=(ModuleLoader&&) = delete;
JS::PersistentRootedObject moduleNamespace; ~ModuleLoader();
JS::PersistentRootedObject fulfill;
JS::PersistentRootedObject reject;
};
struct Fulfilled
{
JS::PersistentRootedObject moduleNamespace;
};
struct Rejected
{
std::exception_ptr error;
};
struct Invalid {};
using Status = std::variant<Evaluating, Fulfilled, Rejected, Invalid>;
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{}};
};
/** /**
* Load the specified module and all module it imports recursively. * Load the specified module and all module it imports recursively.
* *
* @param rq @c globalThis is taken from this @c ScriptRequest. * @param rq @c globalThis is taken from this @c ScriptRequest.
* @param modulePath The path to the file which should be loaded as a module. * @param modulePath The path to the file which should be loaded as a
* @return A future that is fulfilled when the evaluation of the module * module.
* completes. * @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: private:
// Functions used by the `ScriptContext`. // Functions used by the `ScriptContext`.
[[nodiscard]] static bool MetadataHook(JSContext* cx, JS::HandleValue privateValue, [[nodiscard]] static bool MetadataHook(JSContext* cx, JS::HandleValue privateValue,
JS::HandleObject metaObject) noexcept; 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; JS::HandleObject moduleRequest) noexcept;
[[nodiscard]] static bool DynamicImportHook(JSContext* cx, JS::HandleValue referencingPrivate, [[nodiscard]] static bool DynamicImportHook(JSContext* cx, JS::HandleValue referencingPrivate,
JS::HandleObject moduleRequest, JS::HandleObject promise) noexcept; JS::HandleObject moduleRequest, JS::HandleObject promise) noexcept;
RegistryType m_Registry; RegistryType m_Registry;
}; };
class ModuleLoader::CompiledModule
{
public:
CompiledModule(const ScriptRequest& rq, const VfsPath& filePath);
std::tuple<const std::vector<VfsPath>&,
const std::vector<std::reference_wrapper<Result>>&> GetRequesters() const;
void AddRequester(VfsPath importer);
void AddRequester(Result& callback);
void RemoveRequester(Result* toErase);
JS::PersistentRootedObject m_ModuleObject;
private:
std::vector<VfsPath> m_Importer;
std::vector<std::reference_wrapper<Result>> 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<Evaluating, Fulfilled, Rejected, WaitingForFileChange, Invalid>;
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 } // namespace Script
#endif // INCLUDED_SCRIPTMODULELOADER #endif // INCLUDED_SCRIPTMODULELOADER
+263 -35
View File
@@ -17,6 +17,8 @@
#include "lib/self_test.h" #include "lib/self_test.h"
#include "lib/file/vfs/vfs.h"
#include "lib/sysdep/dir_watch.h"
#include "ps/CLogger.h" #include "ps/CLogger.h"
#include "ps/Filesystem.h" #include "ps/Filesystem.h"
#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/FunctionWrapper.h"
@@ -26,11 +28,53 @@
#include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptInterface.h"
#if OS_LINUX
#include <cstdlib>
#endif
#if OS_WIN || OS_WIN64 || OS_MAC || OS_MACOSX
#include <filesystem>
#endif
#include <fstream>
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 class TestScriptModule : public CxxTest::TestSuite
{ {
public: public:
void setUp() void setUp()
{ {
if constexpr (OS_WIN)
wdir_watch_Init();
g_VFS = CreateVfs(); g_VFS = CreateVfs();
TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "_test.scriptinterface" / "module" / "", TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "_test.scriptinterface" / "module" / "",
VFS_MOUNT_MUST_EXIST)); VFS_MOUNT_MUST_EXIST));
@@ -39,6 +83,9 @@ public:
void tearDown() void tearDown()
{ {
g_VFS.reset(); g_VFS.reset();
if constexpr (OS_WIN)
wdir_watch_Shutdown();
} }
void test_StaticImport() void test_StaticImport()
@@ -123,8 +170,9 @@ public:
{ {
ScriptInterface script{"Test", "Test", g_ScriptContext}; ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script}; 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()); TS_ASSERT(!future.IsDone());
g_ScriptContext->RunJobs(); g_ScriptContext->RunJobs();
TS_ASSERT(future.IsDone()); TS_ASSERT(future.IsDone());
@@ -136,8 +184,9 @@ public:
ScriptInterface script{"Test", "Test", g_ScriptContext}; ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script}; 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(); g_ScriptContext->RunJobs();
TS_ASSERT(!future.IsDone()); TS_ASSERT(!future.IsDone());
TS_ASSERT_THROWS_ANYTHING(std::ignore = future.Get()); TS_ASSERT_THROWS_ANYTHING(std::ignore = future.Get());
@@ -148,8 +197,8 @@ public:
ScriptInterface script{"Test", "Test", g_ScriptContext}; ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script}; const ScriptRequest rq{script};
Script::ModuleLoader::Future future0{ auto result{script.GetModuleLoader().LoadModule(rq, "empty.js")};
script.GetModuleLoader().LoadModule(rq, "empty.js")}; Script::ModuleLoader::Future& future0{*result.begin()};
g_ScriptContext->RunJobs(); g_ScriptContext->RunJobs();
TS_ASSERT(future0.IsDone()); TS_ASSERT(future0.IsDone());
@@ -168,8 +217,9 @@ public:
ScriptInterface script{"Test", "Test", g_ScriptContext}; ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script}; const ScriptRequest rq{script};
Script::ModuleLoader::Future future0{ auto result{script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js")};
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 future1{std::move(future0)};
Script::ModuleLoader::Future future2; Script::ModuleLoader::Future future2;
future2 = std::move(future1); future2 = std::move(future1);
@@ -189,11 +239,13 @@ public:
const ScriptRequest rq{script}; const ScriptRequest rq{script};
TestLogger logger; 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"); TS_ASSERT_STR_NOT_CONTAINS(logger.GetOutput(), "blah blah blah");
auto future = std::move(*blabbermouthResult.begin());
TS_ASSERT(!future.IsDone()); 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()); TS_ASSERT(!future.IsDone());
g_ScriptContext->RunJobs(); g_ScriptContext->RunJobs();
@@ -208,8 +260,9 @@ public:
// To silence the error. // To silence the error.
const TestLogger _; 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(); g_ScriptContext->RunJobs();
TS_ASSERT(future.IsDone()); TS_ASSERT(future.IsDone());
TS_ASSERT_THROWS_EQUALS(std::ignore = future.Get(), const std::runtime_error& e, e.what(), 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}; ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script}; const ScriptRequest rq{script};
auto future = script.GetModuleLoader().LoadModule(rq, "export.js"); auto result = script.GetModuleLoader().LoadModule(rq, "export.js");
g_ScriptContext->RunJobs(); g_ScriptContext->RunJobs();
auto& future = *result.begin();
JS::RootedObject ns{rq.cx, future.Get()}; JS::RootedObject ns{rq.cx, future.Get()};
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
@@ -248,17 +302,17 @@ public:
const ScriptRequest rq{script}; const ScriptRequest rq{script};
{ {
auto future = script.GetModuleLoader().LoadModule(rq, "export.js"); auto result = script.GetModuleLoader().LoadModule(rq, "export.js");
g_ScriptContext->RunJobs(); 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)}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
TS_ASSERT(ScriptFunction::CallVoid(rq, moduleValue, "mutate", 12)); 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(); 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)}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
int value{0}; int value{0};
TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value)); TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value));
@@ -272,17 +326,17 @@ public:
const ScriptRequest rq{script}; const ScriptRequest rq{script};
{ {
auto future = script.GetModuleLoader().LoadModule(rq, "export.js"); auto result = script.GetModuleLoader().LoadModule(rq, "export.js");
g_ScriptContext->RunJobs(); 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)}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
TS_ASSERT(ScriptFunction::CallVoid(rq, moduleValue, "mutate", 12)); 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(); 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)}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
int value{0}; int value{0};
TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value)); TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value));
@@ -295,10 +349,10 @@ public:
ScriptInterface script{"Test", "Test", g_ScriptContext}; ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script}; 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(); g_ScriptContext->RunJobs();
auto& future = *result.begin();
JS::RootedObject ns{rq.cx, future.Get()}; JS::RootedObject ns{rq.cx, future.Get()};
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
@@ -324,10 +378,11 @@ public:
ScriptInterface script{"Test", "Test", g_ScriptContext}; ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script}; 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(); g_ScriptContext->RunJobs();
auto& future = *result.begin();
JS::RootedObject ns{rq.cx, future.Get()}; JS::RootedObject ns{rq.cx, future.Get()};
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
@@ -342,10 +397,10 @@ public:
ScriptInterface script{"Test", "Test", g_ScriptContext}; ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script}; 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(); g_ScriptContext->RunJobs();
auto& future = *result.begin();
JS::RootedObject ns{rq.cx, future.Get()}; JS::RootedObject ns{rq.cx, future.Get()};
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
@@ -359,8 +414,10 @@ public:
ScriptInterface script{"Test", "Test", g_ScriptContext}; ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script}; const ScriptRequest rq{script};
auto future = script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js"); auto awaitResult = script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js");
future = script.GetModuleLoader().LoadModule(rq, "export.js"); auto future = std::move(*awaitResult.begin());
auto exportResult = script.GetModuleLoader().LoadModule(rq, "export.js");
future = std::move(*exportResult.begin());
g_ScriptContext->RunJobs(); g_ScriptContext->RunJobs();
JS::RootedObject ns{rq.cx, future.Get()}; JS::RootedObject ns{rq.cx, future.Get()};
@@ -376,10 +433,10 @@ public:
ScriptInterface script{"Test", "Test", g_ScriptContext}; ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script}; 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(); g_ScriptContext->RunJobs();
auto& future = *result.begin();
JS::RootedObject ns{rq.cx, future.Get()}; JS::RootedObject ns{rq.cx, future.Get()};
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
@@ -407,10 +464,10 @@ public:
ScriptInterface script{"Test", "Test", g_ScriptContext}; ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script}; const ScriptRequest rq{script};
auto future = script.GetModuleLoader().LoadModule(rq, "meta.js"); auto result = script.GetModuleLoader().LoadModule(rq, "meta.js");
g_ScriptContext->RunJobs(); g_ScriptContext->RunJobs();
auto& future = *result.begin();
JS::RootedObject ns{rq.cx, future.Get()}; JS::RootedObject ns{rq.cx, future.Get()};
const JS::RootedValue modNamespace{rq.cx, JS::ObjectValue(*ns)}; const JS::RootedValue modNamespace{rq.cx, JS::ObjectValue(*ns)};
@@ -427,15 +484,186 @@ public:
ScriptInterface script{"Test", "Test", g_ScriptContext}; ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script}; 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(); g_ScriptContext->RunJobs();
auto& future = *result.begin();
JS::RootedObject ns{rq.cx, future.Get()}; JS::RootedObject ns{rq.cx, future.Get()};
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
std::string result; std::string returnValue;
TS_ASSERT(ScriptFunction::Call(rq, moduleValue, "fn", result)); TS_ASSERT(ScriptFunction::Call(rq, moduleValue, "fn", returnValue));
TS_ASSERT_STR_EQUALS(result, "Base01"); 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");
} }
}; };