Part of #107.
Problem
NavigationManagerSession (platform/core/navigation/.../NavigationManagerSession.java) is @Scope("session"): a single shared slot (page, pageParams, runLaterQueue) per HTTP session, used to hand off the initial page to whichever ZKNavigationManager desktop bean is constructed next.
When multiple tabs/iframes are opened concurrently in the same session (e.g. a host shell embedding several /page-embed/** iframes at once), each request's setPageLater(page, params) overwrites the previous pending page/params before the corresponding desktop's @PostConstruct (ZKNavigationManager.init()) consumes it. Result: wrong page shown in an iframe, duplicated pages, or a requested page silently dropped. Same race applies to runLaterQueue.
Key insight
The only real callers (PageNavigationController.navigate() / PageEmbedController) resolve the ModelAndView through ChainableUrlBasedViewResolver → InternalResourceView, which renders via RequestDispatcher.forward() to the .zul view. A servlet forward is synchronous, same thread — ZK creates the Desktop and runs ZKNavigationManager.init() (and later ZKNavigationComposer.doAfterCompose(), which drains runLaterQueue) within that same forwarded request, on the same thread that called setPageLater/runLater.
Session scope is therefore unnecessary and actively harmful under concurrency. No token/correlation mechanism is needed — a ThreadLocal naturally isolates each concurrent request (each tab/iframe bootstrap) without touching PageNavigationController/PageEmbedController at all. This supersedes and closes #111 (token propagation is no longer needed).
Change
- Replace the
@Scope("session") Spring bean with a plain ThreadLocal<NavigationManagerSession> holder (getInstance() lazily creates a thread-local instance; keep the class name and public method signatures — setPage, updateNavManager, runLater, executeQueue, getPage, getPageParams — unchanged to preserve the public API).
- Add
NavigationManagerSession.clear() to remove the thread-local entry.
- Register a servlet
Filter (or equivalent request-lifecycle hook) that calls clear() in a finally after each request, so pooled threads don't retain a stale instance.
- Document the new contract on the class Javadoc:
setPageLater/runLater must be called on the same thread that will render/forward into the target ZK desktop (i.e. before an HTTP forward, never before a redirect — a redirect is a new request on a possibly different thread and the thread-local won't survive it). No current caller in the repo violates this; flag it for future callers.
Affected files
platform/core/navigation/src/main/java/tools/dynamia/navigation/NavigationManagerSession.java
platform/core/navigation/src/main/java/tools/dynamia/navigation/NavigationManager.java (Javadoc for setPageLater/runLater)
platform/ui/zk/src/main/java/tools/dynamia/zk/navigation/ZKNavigationManager.java (init(), unchanged logic, just confirm still same-thread)
- new: request-lifecycle filter to clear the ThreadLocal (likely in
platform/core/web or platform/ui/zk-starter)
Part of #107.
Problem
NavigationManagerSession(platform/core/navigation/.../NavigationManagerSession.java) is@Scope("session"): a single shared slot (page,pageParams,runLaterQueue) per HTTP session, used to hand off the initial page to whicheverZKNavigationManagerdesktop bean is constructed next.When multiple tabs/iframes are opened concurrently in the same session (e.g. a host shell embedding several
/page-embed/**iframes at once), each request'ssetPageLater(page, params)overwrites the previous pending page/params before the corresponding desktop's@PostConstruct(ZKNavigationManager.init()) consumes it. Result: wrong page shown in an iframe, duplicated pages, or a requested page silently dropped. Same race applies torunLaterQueue.Key insight
The only real callers (
PageNavigationController.navigate()/PageEmbedController) resolve theModelAndViewthroughChainableUrlBasedViewResolver→InternalResourceView, which renders viaRequestDispatcher.forward()to the.zulview. A servlet forward is synchronous, same thread — ZK creates theDesktopand runsZKNavigationManager.init()(and laterZKNavigationComposer.doAfterCompose(), which drainsrunLaterQueue) within that same forwarded request, on the same thread that calledsetPageLater/runLater.Session scope is therefore unnecessary and actively harmful under concurrency. No token/correlation mechanism is needed — a
ThreadLocalnaturally isolates each concurrent request (each tab/iframe bootstrap) without touchingPageNavigationController/PageEmbedControllerat all. This supersedes and closes #111 (token propagation is no longer needed).Change
@Scope("session")Spring bean with a plainThreadLocal<NavigationManagerSession>holder (getInstance()lazily creates a thread-local instance; keep the class name and public method signatures —setPage,updateNavManager,runLater,executeQueue,getPage,getPageParams— unchanged to preserve the public API).NavigationManagerSession.clear()to remove the thread-local entry.Filter(or equivalent request-lifecycle hook) that callsclear()in afinallyafter each request, so pooled threads don't retain a stale instance.setPageLater/runLatermust be called on the same thread that will render/forward into the target ZK desktop (i.e. before an HTTP forward, never before a redirect — a redirect is a new request on a possibly different thread and the thread-local won't survive it). No current caller in the repo violates this; flag it for future callers.Affected files
platform/core/navigation/src/main/java/tools/dynamia/navigation/NavigationManagerSession.javaplatform/core/navigation/src/main/java/tools/dynamia/navigation/NavigationManager.java(Javadoc forsetPageLater/runLater)platform/ui/zk/src/main/java/tools/dynamia/zk/navigation/ZKNavigationManager.java(init(), unchanged logic, just confirm still same-thread)platform/core/weborplatform/ui/zk-starter)