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();
}
template<typename Requester>
[[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>(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<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>
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<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>)},
{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
// 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<Evaluating>(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<WaitingForFileChange>(m_Status);
}
void ModuleLoader::Future::SetWaiting() noexcept
{
m_Status.emplace<WaitingForFileChange>();
}
void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept
{
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);
}
[[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)
{
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);