Session State
Session state is what the web client still holds after the user moves from one screen to the next. It
owns no route: the mechanism lives in the shell that wraps every route under / — a router outlet
inside a <keep-alive> keyed on a project epoch — together with four Pinia stores, a family of
module-level composables that deliberately sit outside the page lifecycle, and three browser storage
keys. Every value in the client answers the same three questions: does it survive a navigation, does
it survive a project change, and does it survive a browser reload.
The Project Epoch
MainLayout.vue renders the routed page inside a <keep-alive> whose :key is projectEpoch. That
epoch is a plain integer ref declared in the layout component itself — it is not a store value, it
is not provided to anything, and nothing outside the file reads it. Two watchers in the same file are
the only writers, and each adds one to it: a watcher on the project store's projectPath, and a
watcher on the project store's projectVersion.
The key sits on the <keep-alive> element rather than on the page component inside it. Changing it
therefore discards the cache itself along with every page instance held in it and builds a fresh one,
which is why a page may rely on its own mount hook and never watch the project: both Control-Tree
pages call their host's initialize() from onMounted, and that runs once per project rather than
once per visit.
The two inputs move for different reasons.
projectPathis hydrated once at boot by the store's status fetch, assigned by New, Load and Save As from the response each returns, cleared outright by Close, and driven thereafter by a watcher inside the project store on the shared execution-status hub's status payload. A project change made in another tab, from a different browser, or outside the browser altogether therefore bumps the epoch here as well — and so, once, does the boot hydration on a service that already has a project open.projectVersionis a counter the store raises in exactly one place: ReLoad. The backend raises its project-changed event with the path it already had, so the broadcast cannot be told apart from a no-op; the client-side counter is what makes a reload remount.
Save on the current path moves neither, so it does not remount. The epoch only counts up, and because it lives in the layout it starts again at zero on a browser reload — nothing reads its value, only its transitions.
What a Navigation Costs
The <keep-alive> carries no include, exclude or max, so every page reached under / is cached
until the epoch changes. Leaving a route deactivates its component instead of unmounting it, and
returning re-activates the same instance with its in-flight edits and its Control-Tree host intact.
Scroll offsets are not part of that: the router's scrollBehavior resolves every navigation to the
top of the page, and nothing saves an inner scroll position across a deactivation. The two routes
declared outside the layout — the login screen and the not-found catch-all — are not cached at all.
Three consequences the pages are written around:
- A cached page's watchers keep firing while another page is showing. The Control-Tree host compares
the current route name against the one its scope owns before it writes the
?tree=query back, so an off-screen host cannot rewrite the visible page's URL. - A global listener has to be dropped on deactivation, not on unmount. The Execution transport binds its F5–F8 shortcuts on mount and on activation and removes them on deactivation and unmount, so on any other page F5 falls back to the browser's own reload instead of starting the run.
- The Execution, General Setup and Tool House pages each track their own activation in a flag that
feeds the rendering canvas's
activeprop. Such a page keeps its canvas mounted and its connection open while the backend engine stops rendering for it.
The File Explorer page adds a route-leave guard that settles its editor buffer — flushing a pending auto-save, or prompting before discarding manual edits — and can refuse the navigation.
The Stores
Four Pinia stores are created against the single Pinia instance built at boot. None of them is inside
the <keep-alive>, so a project change resets none of them; only a browser reload rebuilds them.
- project holds
projectPath,projectVersion, the admin and project directories, a loading flag and thehasProjectcomputed. Its actions post to the project endpoints and each throws a typed busy error on HTTP 409, which the backend returns when another project file operation is already running — the request is refused rather than queued. The store also owns the subscription to the shared execution-status hub and the watcher that adopts the path that hub broadcasts. - appState holds the debug flag (initialised from whether the page is served from
localhost), the physics-options and physics-licensed flags, the language code with the list of codes the server offers, and the Execution division-visibility record.loadServerPreferences, called from the layout's mount hook, hydrates those three groups in independently guarded steps, so a failing endpoint leaves the rest usable. A division-flag write is applied optimistically and rolled back on failure, with a monotonic sequence number so a late response cannot overwrite newer state. ItssetMessageis a forwarder into the footer's foreground channel. - auth holds whether the login gate is enabled, whether this session is authenticated, the user name, the version string the login screen shows, and a ready flag set once the first status probe resolves. Every transition re-drives the global hub gate described below.
- routineProgress backs the footer's two channels. The foreground channel keeps the latest entry plus a history capped at one hundred entries, which the footer's history button lists; the background channel is a single in-flight job with a message and an optional fraction and keeps no history at all, so it disappears when the job ends. A boot patch mirrors every Quasar toast into the foreground channel, so a toast stays reviewable after it fades.
State Outside the Stores
A second family of state lives at module scope in composables rather than in a store. The module is
evaluated once per page load and its refs are created there, so every caller of the composable
receives the same objects and nothing in a component's lifecycle creates or destroys them.
Three of these hold project data and share one lifecycle idiom — useSpindleCapability,
useSoftNcRunner and useToolHouse. Each exposes an idempotent ensureInstalled() that, on its first
call only, opens a detached effect scope and watches hasProject: the state reloads when a project
appears and is cleared when one closes. Every panel that reads the singleton calls ensureInstalled()
in its own setup, and every call after the first does nothing.
The consequence is the one that matters when reading a panel on screen: a mounted panel is not
refreshed by a change made elsewhere. A project change unmounts and rebuilds every page, but it does
not re-read a singleton — the installer is already installed, and the watch it installed is on a
boolean that the load and new actions leave true. What each singleton holds is replaced only by an
action on the composable itself, or by a consumer that asks for it explicitly: the Tool House page's
mount hook calls the composable's reload(), while the spindle and controller panels call
ensureInstalled() alone. The controller branch is the sharpest case. Its leaf panels fetch their own
tables on mount and the tree builder re-fetches the runner snapshot it grows children from, but the
shared snapshot the panels gate on — the brand, the chain axes, the per-group presence flags and the
object key — is replaced only by a brand switch, an Object-Management install, or a close.
The remaining module singletons carry no project data and exist to join components that are not in one another's tree: the Execution runtime flags the run page publishes for the nav bar, the sentence cursor shared by the Program file panel and the syntax view, the strip-chart group's reload tick and hovered x label, the two cycle-chart cursor marks the sim and sensor chart groups share, the parked cross-panel line jump, the transport's shared reset flag, and the shared-hub registry with its auth gate.
Device-Local State
Three localStorage keys hold state belonging to one browser profile on one machine. None of them is
written into the project file and none is sent to the server.
- The tree-page view preferences key holds, per tree page, the column-visibility record and the two left-dock row states; and, shared across both pages, the dock pixel widths, the editor row height, the strip/step ratio, the two chart legend widths, the strip x-axis mode, the per-panel resize weights, the Control-Tree expansion list, the last selection per page, the two runner-suit checkboxes, and the three device-local panel switches for the 3D canvas, the CWE canvas and the sentence syntax view. Writes are debounced, and two earlier key names are read once as migration seeds when the current key is absent.
- The locale key is a paint-time hint only: the boot sequence applies it before mount so the first frame is already translated, then reconciles against the server value, which wins.
- The File Explorer key holds whether its editor pane is shown, the pane split, the auto-save switch and the sort order.
What is not device-local is easy to mistake for it. The physics-options switch, the interface
language and the Execution division-visibility flags live in the server's UserConfig, held by a
singleton service and written to an XML file in the service's working directory. They are per install,
not per browser: two browsers pointed at the same service read and write the same values. The
machining project is server-side in the same way — one project is loaded at a time and every connected
browser sees it, which is why the status-hub broadcast is what keeps a second tab honest.
The URL carries the rest: the route, the ?tree= selection on the two Control-Tree pages, the browsed
path on the File Explorer route, and the tab segments on the Tool House route. Those
come back on a reload from the address bar, and when a Control-Tree page is opened with no ?tree= the
host lands on the stored last selection for that page, falling back to the page's root when the stored
id is not in the current project's tree.
| What | Navigation | Project change | Browser reload |
|---|---|---|---|
| A page's own component state, including its Control-Tree host | survives | rebuilt | rebuilt |
| The four Pinia stores | survives | survives | rebuilt |
| Module-singleton composables | survives | survives | rebuilt |
The localStorage preferences |
survives | survives | survives |
Route, ?tree=, browsed path and tab segments |
this is what changes | survives | survives |
| Server preferences and the loaded project | survives | server-side | survives |
SignalR Connections
Most hubs are shared singletons built by one factory: the execution-status hub, the CL strip hub, and the four session-message sinks for shell, NC diagnostic, step diagnostic and NC-manipulation diagnostic messages. There is one connection per hub for the whole application, reference-counted by consumer. A component registers as a consumer when it calls the hub's composable and releases on its own unmount; when the count reaches zero the teardown waits out a short grace window, so the remount a project change forces — every consumer dropping and re-adding within the same render flush — keeps its connections rather than renegotiating them.
The project store takes out a consumer registration on the execution-status hub when the store is created and never releases it itself, so that hub keeps a consumer no matter which page is showing.
A global gate sits in front of all of them. While the optional login gate is enabled and the session is not authenticated — and while auth status is still resolving — hubs stay idle rather than negotiating into a 401. A logout or an intercepted 401 closes every connection; permitting again reconnects the ones a caller had asked for, without the caller asking twice. Reconnection is a dense burst of attempts, then a fixed cadence, then a slow tail that never gives up, and tab visibility or window focus starts a fresh burst immediately.
Two kinds of connection are per instance rather than shared:
- Each rendering canvas opens its own connection to the rendering hub on mount and stops it on unmount.
Because pages are cached, navigating away does not close one, and whether the backend engine stops
drawing for it depends on the host. The Execution, General Setup and Tool House pages hand their
activation flag to the canvas's
activeprop, which pauses the engine while the page sits off-screen; the Machine Tool and Mech Builder canvases and the File Explorer's STL preview bind no such prop, and the Execution page's CWE canvas pins it true, so all of those keep rendering until something unmounts them. Collapsing a panel does close one, unless that panel keeps its content mounted: the Execution page's 3D canvas panel does, so it survives a collapse, while the CWE panel beside it does not and its canvas connection goes with it. - The cleanup hub is opened once per Control-Tree host and once on the Tool House page. Its composable also holds the set of IndexService keys that owner registered; on unmount it posts a removal for every one of them and then stops the connection, so a teardown releases the server-side objects the page had indexed even if the hub never connected.
A shared connection therefore closes when its last consumer has been gone longer than the grace window, or when the auth gate shuts. A per-instance connection closes when the component owning it unmounts — which is what a project change does to every page at once, while the shared hubs ride that same remount out inside their grace window.
Boot Order
App.vue renders nothing but the router outlet and holds the wiring that must run exactly once per
page load: subscribe the project hub, then fetch project status. Both sit behind a watcher on the auth
predicate with a latch rather than behind a mount hook, because auth status may still be resolving at
first paint. The router's own guard hydrates auth status before the first navigation and fails open, so
a status hiccup cannot lock out an installation that has no login gate. The layout's mount hook then
hydrates the server preferences. A successful sign-in navigates with a full page load rather than a
router push, so the whole sequence runs again with the authentication cookie present.
Source Code Path
See HiNC App Anatomy for git repository links.
Web Application
HiNC-2025-webservice (Quasar CLI SPA):
wwwroot-src/src/layouts/MainLayout.vue— the project epoch, its two watchers, the keyed<keep-alive>around the router outlet, the footer, and the server-preference hydration call.wwwroot-src/src/App.vue— the auth-gated one-shot wiring: the project hub subscription and the first status fetch.wwwroot-src/src/stores/index.ts— the Pinia instance the four stores are created against.wwwroot-src/src/stores/project.ts— the project path and version, the directories, the file actions and their busy error, the hub subscription, and the watcher that adopts the broadcast path.wwwroot-src/src/stores/appState.ts— the debug and physics flags, the language state, the Execution division config with its optimistic write and sequence guard, and the footer forwarder.wwwroot-src/src/stores/auth.ts— the login-gate state and the hub gate it drives.wwwroot-src/src/stores/routineProgress.ts— the footer's foreground history and its single live background job.wwwroot-src/src/composables/useViewPrefs.ts— the device-local view preferences: the shape, the defaults, the migration seeds and the debounced write.wwwroot-src/src/composables/useSoftNcRunner.ts— the controller singleton: the shared runner snapshot, the object key, and the install-once project watch.wwwroot-src/src/composables/useSpindleCapability.ts— the spindle singleton on the same lifecycle idiom.wwwroot-src/src/composables/useToolHouse.ts— the tool-house singleton and the coalesced reload the page calls on mount.wwwroot-src/src/composables/useSharedHub.ts— the shared-hub factory: consumer counting, the teardown grace window, the auth gate, the retry schedule and the focus recovery.wwwroot-src/src/composables/useExecutionStatusHub.ts— the status, cursor and message payloads the store and the footer read.wwwroot-src/src/composables/useClStripHub.ts— the strip snapshot and update counter the charts watch.wwwroot-src/src/composables/useSessionSinkHub.ts— the four message sinks and their notify-and-re-pull contract.wwwroot-src/src/composables/useCleanupHub.ts— the per-instance cleanup connection and the key set it releases on unmount.wwwroot-src/src/composables/useExecutionTransport.ts— the activation-scoped keyboard shortcuts and the shared reset flag.wwwroot-src/src/composables/useExecutionRuntime.ts— the runtime flags the run page publishes.wwwroot-src/src/composables/useSentenceCursor.ts— the shared source cursor and its install-once watch on step selection.wwwroot-src/src/composables/useStripChartGroup.ts— the group reload tick and the hovered x label.wwwroot-src/src/composables/useCycleSyncMark.ts— the per-group cycle-chart cursor mark the sim and sensor charts share.wwwroot-src/src/composables/useConversionJump.ts— the parked cross-panel jump.wwwroot-src/src/components/RenderingCanvas.vue— the per-instance rendering connection, its mount and unmount lifecycle, and the mounted guard that survives a mid-await teardown.wwwroot-src/src/components/panels/PanelExpansion.vue— the expansion row, and the keep-mounted flag that decides whether a collapse unmounts its content.wwwroot-src/src/components/AppFooter.vue— the two footer channels and the recent-message list.wwwroot-src/src/components/FileExplorer.vue— the explorer's own device-local preference key.wwwroot-src/src/components/controlTree/useControlTreeHost.ts— the per-page host: the route-name guard on the?tree=sync, the landing selection, and the cleanup registrations.wwwroot-src/src/pages/ExecutionPage.vue— the activation flag, the once-per-project initialization, and the panel that keeps the 3D canvas mounted through a collapse.wwwroot-src/src/pages/GeneralSetupPage.vue— the same lifecycle for the equipment host.wwwroot-src/src/pages/ToolHousePage.vue— the page that reloads its singleton explicitly on mount.wwwroot-src/src/pages/MachineToolPage.vue,wwwroot-src/src/pages/MechBuilderPage.vueandwwwroot-src/src/components/StlPreviewPane.vue— the canvases mounted with noactivebinding.wwwroot-src/src/components/execution/StepVolumePanel.vue— the CWE canvas that pinsactivetrue and is stopped only by its own unmount.wwwroot-src/src/pages/FileExplorerPage.vue— the route-leave guard that settles the editor buffer.wwwroot-src/src/pages/LoginPage.vue— the full-page navigation after a successful sign-in.wwwroot-src/src/router/index.ts— the auth guard that hydrates status before the first navigation.wwwroot-src/src/router/routes.ts— which routes render inside the layout and which do not.wwwroot-src/src/boot/auth.ts— the fetch wrapper that turns a 401 into a logged-out store and a bounce to the login screen.wwwroot-src/src/boot/i18n.ts— the cached-locale fast path and the server reconcile.wwwroot-src/src/boot/routine-toast.ts— the toast mirror into the footer channel.wwwroot-src/src/i18n/index.ts— the locale storage key and the single place a locale change is applied.wwwroot-src/src/api/preference.ts— the typed wrappers over the preference endpoints the app-state store hydrates from.Environments/ProjectController.cs— the status, new, load, save, save-as, reload and close endpoints, and the conflict reply that becomes the client's busy error.Environments/PreferenceController.cs— the endpoints behind the server-held preferences.Environments/UserConfig.cs— the persisted user configuration: the physics switch, the language code, the graphic-cache limits, the step-present list and the Execution division config.Environments/UserService.cs— the singleton that owns that configuration and writes it to file.Execution/ExecutionStatusService.cs— the broadcast that carries a project change to every connected browser.Program.cs— where the user-configuration service is registered and its file path fixed.
See Also
- Control Tree — the per-page host this epoch destroys and rebuilds, and the selection it restores from device-local storage
- Program and Hosting — how the SPA is served and hosted, and where the server-side singletons this page reads actually live
- Main Panel — the shell around the router outlet: the menu bar that fires the project actions and the footer that shows their outcome