How the GeoLibre Plugin System Manages Plugins: PluginManager and FloatingPanelRegistry Explained
The GeoLibre plugin system orchestrates independently-lifecycled functionality through two core modules: PluginManager handles registration, activation, and state persistence, while FloatingPanelRegistry manages reactive UI overlays that plugins can render.
GeoLibre treats plugins as first-class citizens with isolated lifecycles and scoped APIs. According to the opengeos/GeoLibre source code, the architecture separates business logic orchestration from UI presentation, enabling developers to build map tools that integrate seamlessly with the desktop application’s React-based interface.
Core Architecture: PluginManager and FloatingPanelRegistry
The plugin architecture centers on two distinct registries that communicate through well-defined APIs.
PluginManager (packages/plugins/src/plugin-manager.ts) serves as the central authority for plugin lifecycles. It maintains a map of registered plugins, tracks active states, handles asynchronous activation with rollback protection, processes deep-link URL parameters, and persists project-specific settings.
FloatingPanelRegistry (packages/plugins/src/floating-panel-registry.ts) provides an imperative API for draggable UI panels. It exposes open/close/focus operations and supplies a reactive snapshot compatible with React’s useSyncExternalStore, ensuring UI components re-render only when panel visibility or stacking order changes.
PluginManager: Lifecycle and State Management
Plugin Registration and Default Activation
Plugins enter the system through the register method in packages/plugins/src/plugin-manager.ts. This method validates the plugin, stores default map control positions, and immediately activates plugins marked with activeByDefault.
// packages/plugins/src/plugin-manager.ts (lines 24-48)
register(plugin: GeoLibrePlugin): void {
// Replace previous instance if hot-reloaded
this.plugins.set(plugin.id, plugin);
this.urlParameterNamesById.set(plugin.id,
normalizeUrlParameterNames(plugin.urlParameterNames));
const defaultPos = plugin.getMapControlPosition?.();
if (defaultPos) this.defaultMapControlPositions.set(plugin.id, defaultPos);
// Mark active if the plugin declares activeByDefault
if (plugin.activeByDefault) {
this.defaultActive.add(plugin.id);
this.active.add(plugin.id);
}
this.notify();
}
The registration process captures URL parameter names for later deep-linking and notifies all subscribers of the state change.
Activation Flow and Async Mounting
The activate method (lines 62-84) handles both synchronous and asynchronous plugin initialization. It scopes the application API to prevent privilege escalation, tracks activation state to prevent concurrent activation attempts, and watches returned Promises to handle mount failures gracefully.
activate(id: string, app: GeoLibreAppAPI): boolean | Promise<boolean> {
const plugin = this.plugins.get(id);
if (!plugin || this.activating.has(id)) return false;
if (this.active.has(id)) return true; // already active
const scopedApp = scopeAppToPlugin(app, id);
this.activating.add(id);
let result: ReturnType<GeoLibrePlugin['activate']>;
try { result = plugin.activate(scopedApp); }
finally { this.activating.delete(id); }
if (result === false) return false; // refused activation
const gen = this.nextActivationGeneration(id);
this.active.add(id);
this.notify();
// Watch Promise results to roll back on failure
const asyncResult = this.watchAsyncActivation(id, result, scopedApp, gen);
if (asyncResult) this.trackActivationResult(id, asyncResult);
return asyncResult ?? true;
}
The watchAsyncActivation helper (lines 98-118) protects against stale callbacks using generation counters. If an async activation fails, it automatically removes the plugin from the active set and cleans up resources.
Scoped Application API
When a plugin receives the application API, GeoLibre scopes it via scopeAppToPlugin (lines 443-489). This wrapper automatically tags UI registrations with the plugin’s ID and prevents plugins from deactivating themselves, avoiding lifecycle loops.
function scopeAppToPlugin(app, pluginId, options = {}): GeoLibreAppAPI {
const scoped = { ...app };
// Tag toolbar menus with owner id
if (app.registerToolbarMenu) {
scoped.registerToolbarMenu = (menu) =>
(app.registerToolbarMenu as any)(menu, pluginId);
}
// Guard against self-deactivation loops
if (app.deactivatePlugin) {
scoped.deactivatePlugin = (targetId) =>
targetId === pluginId ? false : app.deactivatePlugin(targetId);
}
return scoped;
}
Deep-Linking with URL Parameters
The handleUrlParameters method (lines 323-368) enables deep-linking by dispatching URL query parameters to interested plugins. It deduplicates work per context using handledUrlParametersByContext and inFlightUrlContexts, ensuring handlers execute exactly once even during overlapping async dispatches.
for (const [id, plugin] of this.plugins) {
if (!plugin.handleUrlParameters) continue;
const names = this.urlParameterNamesById.get(id) ?? [];
if (!names.some(n => params.has(n))) continue; // plugin not involved
if (!this.active.has(id)) {
const activated = await this.activate(id, app);
if (!activated) continue;
}
await plugin.handleUrlParameters(scopeAppToPlugin(app, id), params);
}
Project State Persistence
GeoLibre captures complete plugin configurations via getProjectState() and restores them through restoreProjectState() (lines 549-595). This system records map control positions, custom plugin settings, and activation states, then reconstructs the exact UI layout when users reopen saved projects.
The restoration process re-applies saved positions, injects stored settings via applyProjectState, and reactivates plugins while watching for async failures using the same watchAsyncActivation mechanism used during normal activation.
FloatingPanelRegistry: UI Management for Plugins
While PluginManager handles logic lifecycle, FloatingPanelRegistry (packages/plugins/src/floating-panel-registry.ts) manages presentation layer concerns for draggable, map-overlay panels.
Registering Floating Panels
Plugins register panels during their activation hook using registerFloatingPanel (lines 60-82). The registry validates required fields, normalizes title strings vs. functions, and returns an unregister callback for cleanup during deactivation.
export function registerFloatingPanel(panel: GeoLibreFloatingPanelRegistration): () => void {
if (!panel.id) throw new Error("Panel id is required");
if (typeof panel.title !== "string" && typeof panel.title !== "function")
throw new Error(`Floating panel "${panel.id}" must have a title`);
titleResolver.set(panel);
registry.set(panel.id, panel);
emit(); // Bump version & notify subscribers
return () => {
if (registry.get(panel.id) === panel) unregisterFloatingPanel(panel.id);
};
}
Controlling Panel Visibility
The registry exposes imperative methods to manipulate the panel stack. openFloatingPanel (lines 102-115) brings panels to the front of the z-order and triggers onOpen callbacks, while closeFloatingPanel removes IDs from the open set and executes onClose hooks.
export function openFloatingPanel(id: string): boolean {
const panel = registry.get(id);
if (!panel) { console.warn("Panel not found:", id); return false; }
const wasOpen = openIds.includes(id);
openIds = [...openIds.filter(o => o !== id), id]; // bring to front
emit();
if (!wasOpen) runHook(id, "onOpen", panel.onOpen);
return true;
}
Focus operations simply reorder the openIds array without triggering open/close lifecycle hooks, allowing users to bring background panels forward without full remounts.
Reactive Snapshots for React Integration
The registry implements the useSyncExternalStore pattern via getFloatingPanelsSnapshot and subscribeFloatingPanels (lines 167-178). This ensures React components re-render only when the set or order of open panels changes, not on every internal registry mutation.
export function getFloatingPanelsSnapshot(): FloatingPanelsSnapshot {
return snapshot; // { openIds, version }
}
export function subscribeFloatingPanels(listener: () => void): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
}
Practical Implementation Examples
Registering a Basic Plugin
Define a plugin that registers a toolbar menu and activates by default:
// my-plugin.ts
import type { GeoLibrePlugin } from "@geolibre/plugins";
export const myPlugin: GeoLibrePlugin = {
id: "my-plugin",
title: "Demo Plugin",
activeByDefault: true,
activate(app) {
console.log("My plugin activated");
const unregister = app.registerToolbarMenu({
label: "Demo",
command: () => alert("Demo command"),
});
return () => unregister(); // Cleanup on deactivation
},
deactivate(app) {
console.log("My plugin deactivated");
},
};
Register the plugin at application startup:
// packages/plugins/src/builtins.ts
import { PluginManager } from "./plugin-manager";
import { myPlugin } from "./my-plugin";
PluginManager.register(myPlugin);
Creating a Floating Panel Plugin
Expose draggable UI by registering a panel during activation:
import { registerFloatingPanel, openFloatingPanel } from "@geolibre/plugins";
export const myPlugin = {
id: "floating-demo",
activate(app) {
const unregister = registerFloatingPanel({
id: "demo-panel",
title: () => "Demo Panel",
render: (container) => {
container.innerHTML = `<p>Hello from a floating panel!</p>`;
},
onOpen: () => console.log("panel opened"),
onClose: () => console.log("panel closed"),
});
openFloatingPanel("demo-panel");
return unregister; // Auto-close on deactivation
},
deactivate(app) {
// Cleanup handled by unregister callback
},
};
Consuming Panels in React Components
Render open panels using the reactive snapshot:
import { useSyncExternalStore } from "react";
import {
getFloatingPanelsSnapshot,
subscribeFloatingPanels,
getFloatingPanel,
} from "@geolibre/plugins";
function FloatingPanels() {
const snapshot = useSyncExternalStore(
subscribeFloatingPanels,
getFloatingPanelsSnapshot,
);
return (
<>
{snapshot.openIds.map((id) => {
const panel = getFloatingPanel(id);
if (!panel) return null;
return (
<FloatingPanelCard key={id} title={panel.title}>
{panel.render(containerRef.current)}
</FloatingPanelCard>
);
})}
</>
);
}
Summary
- PluginManager (
packages/plugins/src/plugin-manager.ts) serves as the central lifecycle authority, handling registration, activation, deactivation, URL deep-linking, and project state persistence for the GeoLibre plugin system. - FloatingPanelRegistry (
packages/plugins/src/floating-panel-registry.ts) provides imperative UI management for draggable panels, exposing open/close/focus operations and reactive snapshots viauseSyncExternalStore. - The scoped application API (
scopeAppToPlugin) isolates plugins by tagging UI registrations with owner IDs and preventing self-deactivation loops. - Async activation protection uses generation counters and
watchAsyncActivationto handle Promise-based mounts, automatically rolling back state on failure. - Project state persistence captures map control positions and custom settings, restoring exact plugin configurations across application sessions.
Frequently Asked Questions
How does GeoLibre handle plugin activation failures?
The PluginManager.activate method in packages/plugins/src/plugin-manager.ts returns false for immediate failures and watches Promise-based activations via watchAsyncActivation (lines 98-118). If an async mount rejects, the manager automatically removes the plugin ID from the active set and cleans up any partial registrations, ensuring the application remains stable.
Can plugins activate other plugins?
Yes, but with restrictions. The scoped application API (scopeAppToPlugin, lines 443-489) wraps the activatePlugin method to prevent a plugin from activating or deactivating itself, which would create lifecycle loops. However, plugins can activate other plugins by ID through the scoped API, and the system tracks concurrent activation attempts via the activating Set to prevent race conditions.
What happens to floating panels when a plugin deactivates?
Floating panels registered via registerFloatingPanel return an unregister callback that should be invoked during the plugin's deactivate hook. This callback automatically removes the panel from the registry and closes any open instances. If the cleanup is omitted, the panel remains in the registry but becomes non-functional once the plugin's render context disappears.
How does the floating panel system optimize React rendering?
The FloatingPanelRegistry implements the useSyncExternalStore pattern (lines 167-178) by exposing getFloatingPanelsSnapshot and subscribeFloatingPanels. The snapshot contains an openIds array and a version counter; React components only re-render when the version changes, which occurs only when panels open, close, or reorder. This prevents unnecessary renders during internal registry mutations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →