diff --git a/src/widget.tsx b/src/widget.tsx index 1507cd0..d3714b5 100644 --- a/src/widget.tsx +++ b/src/widget.tsx @@ -73,9 +73,9 @@ function provideModule(moduleName: string, module: any) { } function invalidateModule(moduleName: string) { - // next requestModule waits for a fresh provideModule (hot reload); called - // synchronously on change:code so consumers created after the update never - // see the stale module + // Next requestModule waits for a fresh provideModule. This is called + // synchronously for both replacement models and trait updates so consumers + // created during hot reload never see the stale module. delete modules[moduleName]; delete moduleFunctions[moduleName]; } @@ -315,6 +315,10 @@ export class Module extends WidgetModel { } initialize(attributes: any, options: any): void { super.initialize(attributes, options); + // A kernel restart replaces the old Module model instead of updating its + // code trait. Invalidate synchronously so consumers created alongside the + // replacement wait for this model rather than using the cached module. + invalidateModule(this.get("name")); this.addModule(); // hot reload: re-import when the kernel ships new module code this.on("change:code change:url change:dependencies", () => { diff --git a/tests/ui/module_hot_reload_test.py b/tests/ui/module_hot_reload_test.py index e9a159b..05b0825 100644 --- a/tests/ui/module_hot_reload_test.py +++ b/tests/ui/module_hot_reload_test.py @@ -24,3 +24,24 @@ def test_module_code_hot_reload(solara_test, page_session: playwright.sync_api.P ipyreact.define_module("hot-reload-module", code_template % 2) display(ipyreact.ValueWidget(_module="hot-reload-module", _type="Label")) page_session.locator(".hot-widget >> text=version 2").wait_for() + + +def test_module_recreated_widget_hot_reload(solara_test, page_session: playwright.sync_api.Page): + # A *new* module widget for a name that is already registered must win + # over the module the registry still holds. Solara closes the per-kernel + # module widgets on a hot reload (a trait update on a closed widget never + # reaches the browser) and creates fresh ones, so without invalidating, + # consumers rendered afterwards keep resolving the previous module. + from solara.server import esm, kernel_context + + ipyreact.define_module("recreate-module", code_template % 1) + display(ipyreact.ValueWidget(_module="recreate-module", _type="Label")) + page_session.locator(".hot-widget >> text=version 1").wait_for() + + # what context.restart() does to the module widgets on a hot reload + kernel_id = kernel_context.get_current_context().id + esm._modules_added_per_kernel[kernel_id]["recreate-module"].close() + + ipyreact.define_module("recreate-module", code_template % 2) + display(ipyreact.ValueWidget(_module="recreate-module", _type="Label")) + page_session.locator(".hot-widget >> text=version 2").wait_for()