Connection recovery and shutdown
After following the Navigation integration guide or the Axis Streaming quickstart, use this recipe to show connection status, recover when Rotatrix restarts, and stop pending work before your application releases its resources. Those guides include client creation and initial connection; this page assumes that client already exists.
Connection management is shared by both interfaces. All four SDKs use
OpenAxisConnectionManager. The connection
owner reconnects the same client and announces current application metadata.
| Integration | What stays attached across retries | What is announced again |
|---|---|---|
| Axis Streaming | Axis-name and frame callbacks | Tags, subscribed axes and focus |
| Navigation | NavigationSession and its application adapters | Tags, navigation capability and focus |
| Both | Both sets of handlers on the same client | Navigation capability plus the axis subscription, tags and focus |
Axis Streaming has no navigation session or pivot cleanup. Navigation keeps its session attached during retries and closes it only when the integration stops.
Connection state
Section titled “Connection state”The manager reports connecting, ready, retrying and stopped. ready means
connection startup and metadata announcements have completed. The integration
uses these notifications to update the application’s status display.
The excerpts below show status handling in the Navigation demos. Their connection manager and status callbacks also apply to Axis Streaming: retain its input callbacks and axis subscription instead of adding navigation objects. The TypeScript streaming example shows the same manager with an axis subscription and no navigation session.
Connect and report status
Section titled “Connect and report status”MyOpenAxisIntegration.start() creates the manager with MyConnectionStatus(app) as its status callback.
Its client and session are retained for the integration’s lifetime; self._metadata reads
the current application state, and the registered callbacks publish later changes:
self.connection = OpenAxisConnectionManager( client, metadata=self._metadata, on_state=MyConnectionStatus(app),)self.client, self.session = client, sessionself._metadata_tasks = set()app.on_navigation_changed = self._metadata_changedapp.on_focus_changed = self._metadata_changedapp.on_operation_changed = self._operation_changedself.networking = asyncio.create_task(self.connection.run())View full implementation: MyOpenAxisIntegration.start →
The manager invokes this callback whenever connection state changes. The handler uses the application API to display that state and any retry delay:
class MyConnectionStatus: def __init__(self, app): self.app = app
def __call__(self, state, error, delay): if state == ConnectionManagerState.RETRYING: self.app.set_status(f'Reconnecting in {delay:.1f}s' + (f': {error}' if error else '')) else: self.app.set_status(state.value)The constructor connects the manager to the metadata snapshot and status handler:
connection = new OpenAxisConnectionManager(client, () => Volatile.Read(ref metadata));connection.StateChanged += StatusChanged;View full implementation: MyOpenAxisIntegration.MyOpenAxisIntegration →
The manager invokes MyOpenAxisIntegration.StatusChanged on its networking
thread. The handler posts to the application’s scheduler before changing the UI:
void StatusChanged(ConnectionManagerState state, Exception? error, TimeSpan? delay) => scheduler.Post(() =>{ if (!stopping) app.SetStatus(state == ConnectionManagerState.Retrying ? $"Reconnecting in {delay?.TotalSeconds ?? 0:F1}s" + (error == null ? "" : $": {error.Message}") : state.ToString().ToLowerInvariant());});void RefreshDiagnostics(object? sender, EventArgs args){ var frame = diagnostics.Presentation(); var key = (frame.Revision, frame.ExpiresAt, diagnostics.Enabled); if (diagnosticKey == key) return; diagnosticKey = key; app.SetDiagnostics(frame, diagnostics.Enabled);}View full implementation: MyOpenAxisIntegration.StatusChanged →
The browser integration creates its manager once, after attaching its session. onState updates the host UI on the browser event loop.
this.connection = new OpenAxisConnectionManager(this.client, { metadata: () => this.metadata(), onState: (state, error, delay) => { app.setStatus( state === "retrying" ? `Reconnecting in ${((delay ?? 0) / 1000).toFixed(1)}s${error ? `: ${String(error)}` : ""}` : state, ); if (state !== "ready") this.diagnostics.clear(); },});View full implementation: MyOpenAxisIntegration.constructor →
MyOpenAxisIntegration.start() starts the connection loop after host
initialization. onState reports status through the application API.
Include <openaxis/connection_manager.hpp>. Keep the client and its scheduler alive
until the manager has stopped. Supply current application metadata through a callback:
OpenAxisConnectionManager connection(client, {[&] { return metadata; }});connection.on_state = [&](const ConnectionStatus& status) { // Update the application's connection indicator.};connection.start();Here metadata is a ConnectionMetadata snapshot owned by the integration.
Supply OpenAxisClientOptions::scheduler for deferred delivery and deadlines.
connection.start() is nonblocking; connection.on_state reports manager
progress, while client.on_error reports transport errors. A client-bound
NavigationSession attaches itself and receives connection transitions automatically.
The metadata provider describes current capabilities, tags, axis subscriptions and focus. The dynamic tags recipe explains how application events refresh it while connected.
Recover current application state
Section titled “Recover current application state”Keep the connection owner alive while Rotatrix is unavailable. It retries and publishes a fresh metadata snapshot after reconnecting. If a user changes tools or focus while offline, announce that current state rather than replaying old transitions. See application context and focus for the event wiring.
For Axis Streaming, replace the saved axis order when a new axis announcement arrives and reset any rate-integration timing at connection and gesture boundaries. Otherwise an idle interval can become a large movement on the first new frame. For Navigation, the session invalidates old connection work; retain the same session rather than attaching another on each reconnect.
Shutdown
Section titled “Shutdown”Networking and queued callbacks can still refer to application resources. The integration stops new events and waits for pending work before destroying those resources, so callbacks cannot access a closed application.
For either interface, detach native event producers, stop networking, and await the run task and any outstanding metadata updates. Retain objects referenced by callbacks until that work finishes.
| Language | Stop the connection owner |
|---|---|
| Python | Await connection.stop() and the retained networking task. If an owned task has not started yet, cancel and await it instead. The quickstart’s console loop is cancelled by Ctrl+C. |
| C# | Await connection.StopAsync() and the task returned by StartAsync(), or cancel the quickstart’s stopping token. Keep the UI dispatcher running while queued work needs it. |
| TypeScript | Await connection.stop() and the retained run promise. The streaming helper returns a cleanup function that also removes focus listeners and awaits metadata updates. |
| C++ | Call connection.stop() on the application thread before destroying callback state; it joins networking and may finish pending callbacks synchronously. |
An Axis Streaming integration can then release its input callbacks and native resources. A Navigation integration also closes its session and drains native cleanup while its scheduler, adapters and viewport still exist.
Navigation cleanup in the demos
Section titled “Navigation cleanup in the demos”The following complete stop methods include the shared transport teardown plus Navigation-specific edit cancellation, session closure and graphics cleanup.
Before destroying its window, the application’s run loop awaits
MyOpenAxisIntegration.stop(). Its fields refer to the NavigationSession, networking task
and updates retained during startup:
async def stop(self): self.app.on_navigation_changed = lambda: None self.app.on_focus_changed = lambda: None self.app.on_operation_changed = lambda: None self.app.finish_object_edit(False) updates = tuple(self._metadata_tasks) for task in updates: task.cancel() # Cancel the owned run task even if shutdown arrives before it starts. self.networking.cancel() await asyncio.gather(self.networking, *updates, return_exceptions=True) self._metadata_tasks.clear() self.session.close() self.session.drain() self.app.on_object_changed = lambda: None self.app.on_operation_changed = lambda: None self.app.on_camera_changed = lambda: None self.app.toggle_diagnostics = lambda: None self.app.diagnostic_frame = lambda: None self.app.diagnostics_enabled = lambda: False self.app.set_status('OpenAxis detached')The frame loop pumps its dispatcher while awaiting MyOpenAxisIntegration.StopAsync()
before closing the raylib window:
public async Task StopAsync(){ stopping = true; app.NavigationChanged = app.FocusChanged = app.ContextChanged = () => { }; app.FinishEdit(false); connection.StateChanged -= StatusChanged; await connection.StopAsync(); await running; try { await Task.WhenAll(updates.ToArray()); } catch (Exception error) { Console.WriteLine(error.Message); } session.Dispose(); session.Drain(); scheduler.Dispose(); app.Rendering -= RefreshDiagnostics; app.CameraChanged = app.ObjectChanged = app.ToggleDiagnostics = () => { }; app.ShowPivot(null); app.ShowObjectPivot(null); app.SetDiagnostics(null, false); app.SetStatus("OpenAxis detached");}The application awaits MyOpenAxisIntegration.stop() while its viewport remains
available for deferred marker cleanup. A browser page-unload handler cannot
reliably await networking cleanup; this path is for application-controlled teardown.
stop(): Promise<void> { if (this.stopping) return this.stopping; this.stopped = true; for (const detach of this.detach.splice(0)) detach(); return (this.stopping = this.shutdown());}private async shutdown(): Promise<void> { try { await this.transition; await this.connection.stop(); await this.running; await Promise.allSettled(this.updates); } finally { this.session.close(); // The default scheduler drains marker cleanup in a microtask. await new Promise<void>((resolve) => queueMicrotask(resolve)); this.app.showPivot(undefined); this.app.showPivot(undefined, true); this.diagnostics.clear(); }}Stop native event subscriptions first. While the host still exists, call connection.stop() to join networking and fail outstanding RPCs, then session.close() to clear pivots and invalidate scheduled navigation work. The native scheduler must outlive both objects. Destroy the integration before its native window. The reference app cancels unfinished native object edits, stops/closes the SDK and removes application callbacks during shutdown. C++ close is synchronous, not awaited.
See connection management for retry policy and ownership. The application separately releases its native event handlers when the window closes.
For a faster edit and test loop, use this teardown path in a development reload command. See reloading during development for choosing a reload mechanism and defining which changes require an application restart.
Verify
Section titled “Verify”- Start the application before Rotatrix. It should connect when Rotatrix becomes available.
- Restart Rotatrix while the application remains open. The integration should reconnect and announce current state.
- For Axis Streaming, confirm a new axis announcement is processed before frames and reconnecting does not integrate the disconnected interval.
- Close the application during a retry or metadata update. Networking should stop, with no later callbacks accessing released resources; Navigation overlays should disappear.