How Projects Reference External Plugin State for URL Parameter Handling in GeoLibre
GeoLibre enables projects to automatically activate external plugins and pass them URL query parameters through a combination of urlParameterNames declarations, PluginManager orchestration, and ProjectPluginState persistence.
Modern web mapping applications frequently rely on deep-link URLs to share specific views and configurations. In GeoLibre, this requirement extends beyond core functionality to external plugins—third-party extensions that may not exist when a project is first created. The opengeos/GeoLibre repository implements a robust system that allows saved projects to reference external plugin state and seamlessly handle URL parameters even for plugins loaded from remote manifests.
This article examines the key mechanisms that make this possible: plugin metadata declarations, centralized parameter dispatch, deduplication logic, and project-state persistence across save and restore operations.
Plugin Metadata: Declaring URL Parameter Interest
Plugins opt into URL parameter handling by declaring the query-string keys they care about. This metadata lives in the GeoLibrePlugin interface defined in packages/plugins/src/types.ts:
export interface GeoLibrePlugin {
id: string;
// Optional: names of query parameters this plugin wants to handle
urlParameterNames?: string[];
// Optional: handler called when matching parameters are present
handleUrlParameters?: (
app: PluginAppAPI,
params: URLSearchParams
) => Promise<void> | void;
// ... other lifecycle methods
}
The urlParameterNames array serves as a registration hook. When any declared parameter appears in the URL, GeoLibre knows to activate that plugin and invoke its handler. This declaration-based approach keeps the system lazy: plugins are only loaded and initialized when actually needed.
The handleUrlParameters method receives a scoped PluginAppAPI and the parsed URLSearchParams, allowing the plugin to read values and update its internal state or trigger UI effects.
Example: Elevation Profile Plugin
The built-in elevation profile plugin demonstrates this pattern in packages/plugins/src/plugins/elevation-profile/index.ts:
export const elevationProfilePlugin: GeoLibrePlugin = {
id: "elevationProfile",
urlParameterNames: ["line"],
async handleUrlParameters(app, params) {
const line = params.get("line");
if (line) {
// Parse polyline and dispatch to UI state
app.dispatch({ type: "elevationProfile/setLine", payload: line });
}
},
};
When a user visits https://app.geolibre.io/?line=encodedPolyline123, the PluginManager detects the line parameter, activates the elevation profile plugin, and passes it the parsed value.
PluginManager: Centralized Registration and Dispatch
The PluginManager class in packages/plugins/src/plugin-manager.ts orchestrates the entire URL parameter flow. It maintains three critical data structures:
urlParameterNamesById: AMap<string, string[]>storing normalized parameter names per plugin ID, built at registration timeactive: ASet<string>tracking currently activated pluginshandledUrlParametersByContext: AMap<string, Set<string>>preventing duplicate invocations for the same URL context
Registration-Time Indexing
When PluginManager.register is called, the manager indexes the plugin's parameter interests:
this.urlParameterNamesById.set(
plugin.id,
normalizeUrlParameterNames(plugin.urlParameterNames),
);
This normalization ensures consistent matching regardless of how parameter names are declared (case, whitespace, duplicates).
Context-Aware Deduplication
The handleUrlParameters method implements sophisticated deduplication to handle async re-entrancy and race conditions. It creates a context key from the serialized query string:
const contextKey = params.toString();
let handledPluginIds = this.handledUrlParametersByContext.get(contextKey);
if (!handledPluginIds) {
handledPluginIds = new Set<string>();
this.handledUrlParametersByContext.set(contextKey, handledPluginIds);
}
Before processing each plugin, the manager checks if it has already handled this context. This prevents duplicate work when the URL changes rapidly or when the method is called multiple times during startup.
Activation and Dispatch Flow
For plugins whose declared parameters intersect the incoming URL, the manager handles activation and dispatch:
for (const [id, paramNames] of this.urlParameterNamesById) {
// Skip if no parameter overlap
const hasMatchingParam = paramNames.some(p => params.has(p));
if (!hasMatchingParam) continue;
// Skip if already handled this context
if (handledPluginIds.has(id)) continue;
// Mark as handled early to prevent races
handledPluginIds.add(id);
// Activate if not already active
if (!this.active.has(id)) {
const activated = await this.activate(id, app);
if (!activated) {
handledPluginIds.delete(id); // Allow retry on failure
continue;
}
}
// Dispatch to plugin handler
const plugin = this.plugins.get(id)!;
try {
await plugin.handleUrlParameters!(
scopeAppToPlugin(app, id),
new URLSearchParams(params),
);
} catch (err) {
handledPluginIds.delete(id); // Allow retry on error
console.warn(`Plugin ${id} failed to handle URL parameters`, err);
}
}
This implementation guarantees that each plugin processes each unique URL context exactly once, even in the face of activation failures or async timing issues.
ProjectPluginState: Persisting External Plugin References
External plugins pose a unique challenge: they are not bundled with the application and must be loaded from remote URLs. GeoLibre solves this through ProjectPluginState, defined in packages/core/src/types.ts:
export interface ProjectPluginState {
// URLs of external plugin manifests to load
manifestUrls: string[];
// IDs of currently active plugins
activePluginIds: string[];
// Plugin-specific serialized state
settings: Record<string, unknown>;
}
The manifestUrls array is the critical link for referencing external plugin state. When a project is saved, this list captures all external plugins that were loaded. When restored, these URLs are fetched and registered before URL parameter handling begins.
Persist and Restore Cycle
The PluginManager provides methods for state serialization and restoration:
// Saving: gather current state
getProjectState(): ProjectPluginState {
return {
manifestUrls: this.externalManifestUrls,
activePluginIds: Array.from(this.active),
settings: Object.fromEntries(
Array.from(this.plugins.entries()).map(([id, plugin]) => [
id,
plugin.getProjectState?.() ?? {},
])
),
};
}
// Loading: restore previous state
async restoreProjectState(state: ProjectPluginState, app: AppAPI): Promise<void> {
// Load external manifests first
for (const url of state.manifestUrls) {
await this.loadExternalManifest(url);
}
// Restore activation status and settings
for (const id of state.activePluginIds) {
await this.activate(id, app);
const plugin = this.plugins.get(id);
const pluginState = state.settings[id];
plugin?.setProjectState?.(pluginState);
}
}
This round-trip ensures that URL-derived settings are preserved. If a plugin's state was originally populated from URL parameters, that state is captured in getProjectState and reapplied via restoreProjectState, making the saved project a faithful reproduction of the original view.
Startup Sequence: External Plugins Before URL Handling
The correct load order is essential to avoid race conditions. The host application follows this sequence:
async function startApp(savedProject?: ProjectPluginState) {
const pluginMgr = new PluginManager();
// 1. Restore external plugins FIRST
if (savedProject) {
await pluginMgr.restoreProjectState(savedProject, appAPI);
}
// 2. Now handle URL parameters—external plugins are available
await pluginMgr.handleUrlParameters(
new URLSearchParams(window.location.search),
appAPI,
);
// 3. Continue with UI initialization
renderUI();
}
This ordering guarantees that external plugins are trusted and registered before the URL parameter dispatcher runs. Without this step, a deep-link referencing an external plugin's parameters would fail to find a matching handler.
Error Handling and Resilience
The PluginManager implements defensive patterns for production reliability:
- Activation failures: If
activate()returns false or throws, the plugin ID is removed fromhandledPluginIds, allowing retry on subsequent URL changes - Handler exceptions: Errors in
handleUrlParametersare caught, logged, and the plugin is marked unhandled for that context - Missing plugins: If a saved project references an external manifest that is no longer available, the load fails gracefully with optional user notification
These protections ensure that one misbehaving plugin cannot break URL parameter handling for others.
Summary
urlParameterNamesandhandleUrlParametersin theGeoLibrePlugininterface let plugins declare and process URL query parameters, implemented inpackages/plugins/src/types.tsPluginManager.urlParameterNamesByIdindexes plugin interests at registration time for efficient lookup, located inpackages/plugins/src/plugin-manager.ts- Context-based deduplication via
handledUrlParametersByContextprevents duplicate invocations and handles async race conditions ProjectPluginState.manifestUrlspersists references to external plugins, ensuring they are loaded and trusted before URL parameter dispatchgetProjectStateandrestoreProjectStateenable round-trip persistence of plugin settings, including those derived from URL parameters
Frequently Asked Questions
How does GeoLibre know which plugins to activate for a given URL?
The PluginManager compares incoming query parameter names against the urlParameterNamesById map built at plugin registration. Any plugin whose declared parameters intersect the URL is activated automatically, as implemented in packages/plugins/src/plugin-manager.ts.
What happens if an external plugin fails to load from its manifest URL?
During restoreProjectState, manifest loading errors are caught and logged. The plugin is skipped, and URL parameter processing continues for other plugins. The error does not block the entire project load, though the user may see a notification about missing functionality.
Can a plugin handle URL parameters without declaring urlParameterNames?
Yes, but with limitations. Plugins without urlParameterNames will not be automatically activated based on URL content. They can still access window.location.search directly, but they miss the activation orchestration and deduplication benefits provided by the PluginManager.
How does GeoLibre prevent a plugin from processing the same URL parameters multiple times?
The manager creates a context key from the serialized query string and tracks handled plugins per context in handledUrlParametersByContext. Each plugin ID is added to this set before activation begins, blocking re-entrancy even if the method is called asynchronously during an existing run.
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 →